Vim. 疑似テキストオブジェクト自作。本物に近づけたい場合

ci(da{ のようなノリで日本語の全角括弧の中身やそのまとまりを簡単に選択したり消去する機能――擬似的にテキストオブジェクトを新たに追加するということに挑戦した。

プラグインにするほど大げさな話ではないだろうから、コンパクトにできるのではないかと期待していたがそうでもなかった。自分にとってはまあまあ過酷な道のりだった。

テキストオブジェクトを追加するということは、最終的に [開始行, 開始カラム, 終わりの行, 終わりのカラム] を返す関数を作り、それを オペレーター待機モードとビジュアルモードから呼べるようにキーマップを設定する、ということが必要になる。

目次

キーマップでどうにかする

まずはなるべく簡単に済ます方法を探す。

" 全角のかぎカッコを操作しやすくする例
xnoremap ik :<C-u>execute 'normal! ' .  v:count1 . 'T「v' . (v:count1 + (v:count1 -1)) . 't」'<CR>
onoremap ik :normal vik<CR>
xnoremap ik :<C-u>execute 'normal! ' .  v:count1 . 'F「v' . (v:count1 + (v:count1 -1)) . 'f」'<CR>
onoremap ak :normal vak<CR>

上のような簡単なキーマップを登録しておけば、cik で全角カッコの中身を消すとか、vak で選択、yik で yank するということはできる。

参考: Custom pseudo-text objects · GitHub

ただし、このやり方は括弧の中か閉じ括弧上にカーソルが置かれている必要がある。

開き括弧上だと何も起きないのは少し残念な気がしないでもないが、dw (diwではなく) で単語全部を消したいなら単語の先頭に移動しなさいとか、 da' が思ってたのと違うとなりがち (おそらく大多数が期待するのは d2i' の結果) みたいな例を考えると、別に括弧の中にカーソルを置いてから操作してくださいと言われるのは理不尽でもないとは思う。

プラグインを使う

2つ入れないといけない。これは開き括弧の上にカーソルがあっても動作するし、ネストにも対応している。カウントは未対応。

長年使われているのでおおかたこれらで十分なのだろう。しかし、どうしても気になることがあった。

デフォルトの di( の動き

通常、Vim で di( またはdi), dib とやると ()の外側にいても()の中身を削除することができる。違う行にいても現在のカーソル位置よりも右 (forward) でさえあればたとえ100万行先だろうが一瞬でそこにすっ飛んでいって削除してしまう。

|     (hoge)
↑カーソルがここにいても消せる

さらに、

    |
function! s:hoge(fuga) abort
行をまたいで前方の()も消せる

() だけでなく、{} [] <> でも同じ動作をする (html や xml のタグ(ditとかvatとか)もほとんど同じだが、行跨ぎはできない)。

妙に惹かれる。ワクワクしてしまう。

テキストオブジェクト自作。外から括弧を見つけて範囲を設定する

|   「こんにちは」

カーソルが よりも左にあったとしても、たとえ行を跨いでいたとしても、「こんにちは」 を見つけるには?

最初から を検索しようとすると、当然うまくいかない。「」 の中にカーソルがあるときはどうするの?という話になる。

では最初はカーソルより左にある を検索して、見つかったらカーソルより右にある を検索して…などと色々なパターンを考えてみる。

今のところ上手くいっているのは以下の方法。

vim script

s:GetPairRange():  
  位置取得のメインの関数

s:FindMatchPair(): 
  %を使って移動して位置を取得する
  移動できれば [start_line, start_col, end_line, end_col] を返す
  移動できなければ [0,0,0,0]を返す

s:FallbakcSearchFoward():
  括弧の範囲の中にカーソルがないときに使われる
  カーソルよりも前方の `「` を検索し、あれば s:FindMatchPair()を試みる関数

function! s:GetPairRange(open_char, close_char) abort
    " ①: カーソル下の1文字が開閉どちらかの括弧だったらそのまま範囲設定へ
    if v:count1 == 1 && strchars(a:open_char) == 1 && strchars(a:close_char) == 1
        let char = matchstr(getline('.'), '.', col('.')-1)
        if char ==# a:open_char || char ==# a:close_char
            return s:FindMatchPair(line('.'), col('.'), a:open_char, a:close_char)
        endif
    endif

    " ②: カーソルより左の'「'を検索。括弧の中にいる想定
    let b_open = searchpos(a:open_char, 'bnW')
    if b_open ==# [0,0]
        " fallback 処理。カーソルより左に開き括弧が存在しない時点で前方だけが候補になる
        return s:FallbackSearchForward(a:open_char, a:close_char)
    endif

    " ③: ②をクリアしたのでカーソルより右の'」'を検索。括弧の中にいる想定
    let f_close = searchpos(a:close_char, 'nW')
    if f_close ==# [0,0]
        " fallback 処理。'「」'に囲まれていないのでカーソルよりも右の'「'を検索する
        return s:FallbackSearchForward(a:open_char, a:close_char)
    endif

    " ④: ②と③をクリア = カーソルの前後に開閉の括弧が見つかった場合
    let [lnum, col] = searchpairpos(a:open_char, '', a:close_char, 'nW')

    return s:FindMatchPair(lnum, col, a:open_char, a:close_char)
endfunction

function! s:FindMatchPair(lnum, col, open_char, close_char) abort
    if a:lnum == 0
        return [0,0,0,0]
    endif

    " カーソルを移動してから % コマンドで対応する括弧にジャンプ
    call cursor(a:lnum, a:col)
    keepjumps normal! %

    " 移動後の位置を取得
    let pair_pos = getpos('.')

    " 移動していなければペアなし
    if a:lnum == pair_pos[1] && a:col == pair_pos[2]
        return [0,0,0,0]
    endif

    return [a:lnum, a:col, pair_pos[1], pair_pos[2]]
endfunction

function! s:FallbackSearchForward(open_char, close_char) abort
    " 前方検索は開きが見つかったらすぐペアを探す (本家同様ネストは無視する)
    let s:FallbackForwardFlag = 1

    for idx in range(1, v:count1)
        let f_open = searchpos(a:open_char, 'nW')
        if f_open ==# [0,0]
            return [0,0,0,0]
        else
            " 見つかったら括弧1つ分先に移動
            call cursor(f_open[0], f_open[1] + strlen(a:open_char))
        endif
    endfor

    return s:FindMatchPair(f_open[0], f_open[1], a:open_char, a:close_char)
endfunction

ノーマルモードの % の移動を使うとかなり楽ができる。絶対に正しく対応する括弧へ飛んでくれる。ネストが正しくないと移動しないので明快だ。

どの段階で前方の を探しに行くべきなのか悩んだが、これで十中八九うまくいく。思ったとおりの、() {} [] らと同じような動作になる。

最初から searchpairpos() を使おうとすると検索に時間がかかってうまくいかない。かといってこれがないとネストに上手く対応できない。愚直にwhileで1つずつ探すのも重い。

なので、カーソル下に括弧があるか?→括弧で囲まれているか?→というようにどんどん外堀を固めていき、最終的にネストしているかどうかわからないときだけ searchpairpos() を使うようにするとまあまあまともな速度で処理されるようになる。

また、前方検索(この取り組みのきっかけ)は、フォールバック的な扱いにしておくと具合が良い。しょっぱなから検索するようにしても上手くいかないのは前述の通り。

このように位置を取得してようやくその範囲を選択したり、削除したり、 yank したりする対象として扱うことができるようになる。

これはまだ序の口で、ここから先にはまだまだ対処しなければならない問題が数多く存在する。デフォルトの di( にはまだ遠く及ばない。

現実的な妥協案を受け入れれば4行のキーマップで済むようなことを、浅はかな思いつきを達成するために百行超のスクリプトを作らなければならなくなっている。

範囲を受け取った後 (キーマップに設定する関数本体)

" キーマップに設定する関数
function! TextobjBlock(mode, open_char, close_char)
    " v:operator は「最後に使われたオペレータ」c, d, y のときに必要。 visual mode のときは不要なので無効化
    let operator = mode() =~# '^[vV]$' ? '' : v:operator
    let save_pos = getpos('.')
    let s:FallbackForwardFlag = 0
    call setpos("'`", getpos('.'))
    for idx in range(1, v:count1)
        let [l1, c1, l2, c2] = s:GetPairRange(a:open_char, a:close_char)

        " 前方検索 のフォールバックが発生していたら現在設定されている l1, c1, l2, c2 が既に最終的な値になっているので打ち止め
        if s:FallbackForwardFlag == 1
            break
        endif

        " v:count1 が実際の階層よりも大きい場合は何もしないので l1 == 0 になるまで回してしまう
        " 気を利かせて1つ前の値を使ったりはしない
        if l1 == 0
            break
        endif

        call cursor(l1, c1)
        execute "normal! \<BS>"
    endfor

    if l1 == 0
        call setpos('.', save_pos)
        if  operator ==# 'c'
            " 括弧が見つからないなら ci, ca でインサートモードに入らないようにする
            augroup c_cancel
                autocmd!
                autocmd InsertEnter * ++once call feedkeys("\<Esc>\<Right>")
            augroup END
        endif
        return 0
    endif

    let s:FallbackForwardFlag = 0
    let [l1, c1, l2, c2] = s:OrderPairPos(l1, c1, l2, c2)

    " 選択マーク設定
    call setpos("'<", [0, l1, c1, 0])
    call setpos("'>", [0, l2, c2, 0])
    normal! gv

    if a:mode ==# 'i'
        if l1 == l2 && c2 - c1 == strlen(a:open_char)
            if operator ==# 'c' || operator ==# 'd' 
                call s:EvacuateRegs(s:del_regs)
                call s:SetRestoreAutoCmd()
                " 中身が空のときはダミー文字を置いて cik で中身の編集、dik は移動のみ、という動作を可能にする
                let dammy = '_'
                normal! v
                execute "normal! i" . dammy
                normal! v
            elseif operator ==# 'y'
                call s:EvacuateRegs(s:yank_regs)
                call s:SetRestoreAutoCmd()
            else
                normal! v
                call setpos('.', save_pos)
                return 0
            endif
        else
            execute "normal! o\<Space>o\<Bs>"
        endif
    endif
    return 1
endfunction

let s:saved_regs = {}
let s:del_regs = ['"', '-', '+', '*']
let s:yank_regs = ['"', '+', '0']

function! s:EvacuateRegs(regs) abort
    for r in a:regs 
        let s:saved_regs[r] = getreginfo(r)
    endfor
endfunction

function! s:SetRestoreAutoCmd() abort
    " レジスタ復元用オートコマンドをオペレータ発動後に1度だけ起動
    augroup restore_regs
        autocmd!
        autocmd TextYankPost * ++once call s:RestoreRegs()
    augroup END
endfunction

function! s:RestoreRegs() abort
    if exists('s:saved_regs')
        for r in keys(s:saved_regs)
            call setreg(r, s:saved_regs[r])
        endfor
        let s:saved_regs = {}
    endif
endfunction

inner のときの範囲の狭め方

ここまでで取得できたのは 「から」までの範囲 の座標なので、vak, daka の話だ。i (inner) の場合は範囲を狭めなくてはならない。

対象は全角の括弧つまりマルチバイト文字のためカラム位置の計算がめんどくさい。help を読んだり検索してもなかなかうまいやり方が見つからない。 chat は vim script にあまり詳しくないらしい。

今回は全角1文字限定なので、とりあえず 3バイト分を足し引きしてやればうまくいきそうだと思っていたが、そう簡単ではなかった。行跨ぎに対応しようとすると途端にめんどくさくなる。

そこで、execute "normal! \<BS>"execute "normal! \<Space>" を使って楽をすることにする。これらは規定の設定で行を跨いで1文字分だけ左と右に移動してくれる。 hl はオプションで vimrc に書かないと行を跨げないことになっている。なるべく余分な設定を増やさないようにするために、\<BS>' と <Space>` を使う。

中身がない「」に対して

中身がない「」に対して、cik (kをマッピングするとして) を実行するとどうなるかというと、(|) のように、カーソルが () の間に入って編集を始めることができる。これは地味にありがたい。

しかし、巷の疑似テキストオブジェクト関数はあくまでもビジュアルモードで範囲選択した状態で仕事を終えるため、「」 のように中身がないと 「」 ごと選択している状態になり、cik とすると括弧をまるごと消してしまう。

ここでコントロールできるのは、ほぼ範囲選択だけであり、この関数の中でなにかしても直後に cd の削除操作は問答無用で発生してしまう。

ダミーテキストと v:operator

そこで今のところ、v:operator で直前のオペレーターを取得し、c d のときはダミーを置いて1文字削除させるようにしている。

この方法は削除レジスタが使われてしまう。そのため、空の括弧に対して、dik で移動して pで貼り付けとか、cik で移動して <C-r>" で貼り付けといった操作をしたいときに通常と異なる対応を求められる。

レジスタを使ってごまかそうにも、実際に入力するときしかレジスタは指定できない。 この関数の中でブラックホールレジスタを使ってくれとお願いすることは不可能だ。

そのため、この関数ができることはレジスタを保存しておいて、オペレーターの動作が終わった後にしれっと復元することぐらいのものである。ぐらいのもの、と言ったが、実際、空の() に対する ci( はレジスタに対して何も変更を加えないので、希望通りの結果を得ることはできている。

また、めんどくさいことに v:operator はあくまでも「直前に使われたオペレーター」なので、cd でなにかした後に空の 「」 に対して vik すると、律儀にダミーテキストを挿入してくれてしまう(もちろん削除は行われない)。

これを避けるために、ビジュアルモードの場合は v:operator を無視するように設定しておかないといけない。

カウント対応

ネストとカウントに関しては日本語文章に出てくる全角の括弧なのだから無理して対応する必要がないのかもしれない。しかし、対応していなければそれはそれでなにか欠損感がにじみ出てくる。

カウントはとりあえずループさせておけばなんとかなるだろうと思って実装し、今のところは不具合は起きていない(気づいていない)。

ネストの中にいるときにカウントを使えばどんどん外側へ向かって選択範囲を拡大できる。

前方検索に関しては、とにかく N番目の開き括弧 を探すようにしている。これは ( ( (hello) (world) ) ) の左側にカーソルがある状態で v3i( などとしたときとだいたい同じように動作する。

ここまでで普通に使う分には問題なく動作する。

ネスト対応 searchpairpos() の遅さが気になり始める

ネストの対応は searchpairpos()% のおかげで実現できたが、色々なケースをテストしているうちに行数をバカみたいに増やしたときやネストを過剰に深くしたときのもっさり感が気になり始める。どうしても di( の鬼のような速さと比べてしまう。

     1 「
     2   「おはよう」  「おやすみ」
     3   「おはよう」  「おやすみ」
     .    ...
100000 」

このようなネスト状態で行数が増えると、searchpairpos() は徐々に遅くなる。検索対象が増えるから当然なのかもしれないが、お構いなしに高速な % や デフォルトの di( がいる以上はどうしても気になる。試しに vim9script にしてみたが効果はほとんどなかった。

そこで正規表現を使って対象にすべき括弧を特定する方法を考えてみた。最終的に % を使えるという大前提が必要なため、万能ではないが、searchpairpos() と同じように正しいネストを判定できている(と今は思っている)し、速度的には満足なので、とりあえずこれを採用する。後でバグに気づくかも知れない。

はっきりと違いを体感し始めるのは5000行ぐらいのところからだろうか。実用上は何も違いがないだろう。

function! s:SearchNest(open_char, close_char) abort
    " nest_regex_open    = '「\_[^」]\{-}「'
    " nest_regex_close = '」\_[^「]\{-}\zs」'
    let nest_regex_open  = '\V' . a:open_char . '\_[^' . a:close_char . ']\{-}' . a:open_char
    let nest_regex_close = '\V' . a:close_char . '\_[^' . a:open_char . ']\{-}\zs' . a:close_char

    " 大外までの括弧のまとまりが正しくネストされおり、後で % コマンドがきちんと判定してくれるという前提で候補を選ぶ。あくまでも片割れ発見機。
    " 最終的に % で移動できれば正解。できなければネストが崩れているので範囲は[0,0,0,0]となって親関数は終了する
    " ① ?...x ( a ) y ( b ) z...?
    " この関数が使われるとき、カーソルは a, b のような位置にはいないことが確定している。SearchPosWithRegex() によってそう仕組まれている。
    " ...) ) ...x ( a ) y ( b ) z...?
    " ②だから、この状況で 左側に閉じの連続が見つかるということは、そこで一旦ネストが区切られていると言える。
    " ...) ) ...x ( a ) y ( b ) z... ) ) ← こいつが所属エリアの大外である
    " そうなると、カーソルよりも右にある閉じの連続の終わりの方へ移動すれば % で正しいペア選択をできるはず…という考え方。
    " 逆に、左に連続閉じがないということは、左の連続開きを検索すればほぼ確実に大外を掴めるはず。
    
    let has_close_run_left =  search(nest_regex_close, 'bnW') 
    return has_close_run_left ?  searchpos(nest_regex_close, 'W') : searchpos(nest_regex_open, 'bW') 
endfunction

function! s:SelectProperly(l_open, r_close, r_open, l_close) abort
    " 簡単にネスト検索できるように選択肢を狭めるための手続き
    let lnum = line('.')
    let col = col('.')

    if a:l_close[0] == 0
        " 左の閉じがないのだから、見つけた左の開きを採用するしかない
        let open_pos = a:l_open
    else
        " 行が違う場合
        if lnum - a:l_open[0] < lnum - a:l_close[0]
            let open_pos = a:l_open
        elseif  lnum - a:l_open[0] > lnum - a:l_close[0]
            let open_pos = [0, 0]

        " 同一行の場合
        elseif a:l_open[0] == a:l_close[0]
            if col - a:l_open[1] < col - a:l_close[1]
                " ) ( *
                let open_pos = a:l_open
            else
                " ( ) *
                let open_pos = [0, 0]
            endif
        endif
    endif

    if a:r_open[0] == 0
        " 右の開きがないのだから、見つけた右の閉じを採用するしかない
        let close_pos = a:r_close
    else
        " 行が違う場合
        if a:r_close[0] - lnum < a:r_open[0] - lnum
            let close_pos = a:r_close
        elseif a:r_close[0] - lnum > a:r_open[0] - lnum
            let close_pos = [0,0]

        " 同一行の場合
        elseif a:r_close[0] == a:r_open[0]
            if a:r_close[1] - col < a:r_open[1] - col
                " * ) (
                let close_pos = a:r_close
            else
                " * ( )
                let close_pos = [0, 0]
            endif
        endif
    endif

    return [open_pos, close_pos]
endfunction

function! s:SearchPosWithRegex(open_char, close_char, l_open, r_close) abort
    " 左側の閉じと右側の開き括弧を検索
    let l_close = searchpos(a:close_char, 'bnW')
    let r_open = searchpos(a:open_char, 'nW')

    " a:l_open と a:r_close が[0,0] でこの関数に渡ってくることは基本的にない
    " if a:l_open[0] == 0 && a:r_close[0] == 0
    "     return [0, 0]
    " endif

    let [open_pos, close_pos] = s:SelectProperly(a:l_open, a:r_close, r_open, l_close)
    if open_pos[0] == 0 && close_pos[0] == 0
        " これを使うとき、カーソルは所属がわかりにくい位置にある
        " 連続した括弧(「.*「 または 」.*」みたいな)の端が正解になる
        return  s:SearchNest(a:open_char, a:close_char)

    " 所属階層の開き括弧の近くにカーソルあり
    elseif open_pos[0] != 0 && close_pos[0] == 0
        return open_pos

    " 所属階層の閉じ括弧の近くにカーソルあり
    elseif open_pos[0] == 0 && close_pos[0] != 0
        return close_pos

    " どれでもなければ引数のa:l_openが正解ということになる
    else
        return a:l_open
    endif
endfunction

一旦完成形

vimrc に書いておけば動くはず。もっと簡素にできるかと思っていたが200行を超えてしまった。

キーマップの例)
onoremap ik <Cmd>call TextobjBlock('i', '「', '」')<CR>
xnoremap ik <Cmd>call TextobjBlock('i', '「', '」')<CR>
onoremap ak <cmd>call textobjblock('a', '「', '」')<CR>
xnoremap ak <cmd>call textobjblock('a', '「', '」')<CR>

function! TextobjBlock(mode, open_char, close_char)
    let operator = mode() =~# '^[vV]$' ? '' : v:operator
    let save_pos = getpos('.')
    let s:FallbackForwardFlag = 0
    call setpos("'`", getpos('.'))
    for idx in range(1, v:count1)
        let [l1, c1, l2, c2] = s:GetPairRange(a:open_char, a:close_char)
        if s:FallbackForwardFlag == 1
            break
        endif

        if l1 == 0
            break
        endif

        call cursor(l1, c1)
        execute "normal! \<BS>"
    endfor

    if l1 == 0
        call setpos('.', save_pos)
        if  operator ==# 'c'
            augroup c_cancel
                autocmd!
                autocmd InsertEnter * ++once call feedkeys("\<Esc>\<Right>")
            augroup END
        endif
        return 0
    endif

    let s:FallbackForwardFlag = 0
    let [l1, c1, l2, c2] = s:OrderPairPos(l1, c1, l2, c2)

    call setpos("'<", [0, l1, c1, 0])
    call setpos("'>", [0, l2, c2, 0])
    normal! gv

    if a:mode ==# 'i'
        if l1 == l2 && c2 - c1 == strlen(a:open_char)
            if operator ==# 'c' || operator ==# 'd' 
                call s:EvacuateRegs(s:del_regs)
                call s:SetRestoreAutoCmd()
                let dammy = '_'
                normal! v
                execute "normal! i" . dammy
                normal! v
            elseif operator ==# 'y'
                call s:EvacuateRegs(s:yank_regs)
                call s:SetRestoreAutoCmd()
            else
                normal! v
                call setpos('.', save_pos)
                return 0
            endif
        else
            execute "normal! o\<Space>o\<Bs>"
        endif
    endif
    return 1
endfunction

let s:saved_regs = {}
let s:del_regs = ['"', '-', '+', '*']
let s:yank_regs = ['"', '+', '0']

function! s:EvacuateRegs(regs) abort
    for r in a:regs 
        let s:saved_regs[r] = getreginfo(r)
    endfor
endfunction

function! s:SetRestoreAutoCmd() abort
    augroup restore_regs
        autocmd!
        autocmd TextYankPost * ++once call s:RestoreRegs()
    augroup END
endfunction

function! s:RestoreRegs() abort
    if exists('s:saved_regs')
        for r in keys(s:saved_regs)
            call setreg(r, s:saved_regs[r])
        endfor
        let s:saved_regs = {}
    endif
endfunction

function! s:GetPairRange(open_char, close_char) abort
    if v:count1 == 1 && strchars(a:open_char) == 1 && strchars(a:close_char) == 1
        let char = matchstr(getline('.'), '.', col('.')-1)
        if char ==# a:open_char || char ==# a:close_char
            return s:FindMatchPair(line('.'), col('.'), a:open_char, a:close_char)
        endif
    endif

    let b_open = searchpos(a:open_char, 'bnW')
    if b_open ==# [0,0]
        return s:FallbackSearchForward(a:open_char, a:close_char)
    endif

    let f_close = searchpos(a:close_char, 'nW')
    if f_close ==# [0,0]
        return s:FallbackSearchForward(a:open_char, a:close_char)
    endif

    let [lnum, col] = s:SearchPosWithRegex(a:open_char, a:close_char, b_open, f_close) 

    return s:FindMatchPair(lnum, col, a:open_char, a:close_char)
endfunction

function! s:FallbackSearchForward(open_char, close_char) abort
    let s:FallbackForwardFlag = 1

    for idx in range(1, v:count1)
        let f_open = searchpos(a:open_char, 'nW')
        if f_open ==# [0,0]
            return [0,0,0,0]
        else
            call cursor(f_open[0], f_open[1] + strlen(a:open_char))
        endif
    endfor

    return s:FindMatchPair(f_open[0], f_open[1], a:open_char, a:close_char)
endfunction

function! s:FindMatchPair(lnum, col, open_char, close_char) abort
    if a:lnum == 0
        return [0,0,0,0]
    endif

    call cursor(a:lnum, a:col)
    keepjumps normal! %

    let pair_pos = getpos('.')

    if a:lnum == pair_pos[1] && a:col == pair_pos[2]
        return [0,0,0,0]
    endif

    return [a:lnum, a:col, pair_pos[1], pair_pos[2]]
endfunction

function! s:OrderPairPos(l1, c1, l2, c2) abort
    if (a:l2 < a:l1) || (a:l2 == a:l1 && a:c2 < a:c1)
        return [a:l2, a:c2, a:l1, a:c1]
    endif

    return [a:l1, a:c1, a:l2, a:c2]
endfunction

function! s:SearchPosWithRegex(open_char, close_char, l_open, r_close) abort
    let l_close = searchpos(a:close_char, 'bnW')
    let r_open = searchpos(a:open_char, 'nW')
    let [open_pos, close_pos] = s:SelectProperly(a:l_open, a:r_close, r_open, l_close)
    if open_pos[0] == 0 && close_pos[0] == 0
        return  s:SearchNest(a:open_char, a:close_char)
    elseif open_pos[0] != 0 && close_pos[0] == 0
        return open_pos
    elseif open_pos[0] == 0 && close_pos[0] != 0
        return close_pos
    else
        return a:l_open
    endif
endfunction

function! s:SelectProperly(l_open, r_close, r_open, l_close) abort
    let lnum = line('.')
    let col = col('.')

    if a:l_close[0] == 0
        let open_pos = a:l_open
    else
        if lnum - a:l_open[0] < lnum - a:l_close[0]
            let open_pos = a:l_open
        elseif  lnum - a:l_open[0] > lnum - a:l_close[0]
            let open_pos = [0, 0]

        elseif a:l_open[0] == a:l_close[0]
            if col - a:l_open[1] < col - a:l_close[1]
                let open_pos = a:l_open
            else
                let open_pos = [0, 0]
            endif
        endif
    endif

    if a:r_open[0] == 0
        let close_pos = a:r_close
    else
        if a:r_close[0] - lnum < a:r_open[0] - lnum
            let close_pos = a:r_close
        elseif a:r_close[0] - lnum > a:r_open[0] - lnum
            let close_pos = [0,0]

        elseif a:r_close[0] == a:r_open[0]
            if a:r_close[1] - col < a:r_open[1] - col
                let close_pos = a:r_close
            else
                let close_pos = [0, 0]
            endif
        endif
    endif
    return [open_pos, close_pos]
endfunction

function! s:SearchNest(open_char, close_char) abort
    let nest_regex_open  = '\V' . a:open_char . '\_[^' . a:close_char . ']\{-}' . a:open_char
    let nest_regex_close = '\V' . a:close_char . '\_[^' . a:open_char . ']\{-}\zs' . a:close_char
    let has_close_run_left =  search(nest_regex_close, 'bnW') 
    return has_close_run_left ?  searchpos(nest_regex_close, 'W') : searchpos(nest_regex_open, 'bW') 
endfunction