ハイパーマッスルエンジニア

Vim、ShellScriptについてよく書く

NeoVimのFloating Windowで遊ぶためのTips

f:id:rasukarusan:20191205203259p:plain
NeoVimのFloating Windowで遊ぶためのTips

この記事はVim3 Advent Calendar 2019 6日目の記事です。

NeoVimに乗り換えるきっかけにもなった、Floating Window。
このFloating Window、めちゃくちゃ遊べる要素があるなと思い、こんなものを作って遊んでいた。

https://user-images.githubusercontent.com/17779386/69766815-1199ba80-11bd-11ea-9a90-da266c66e44f.gif
私がfloating windowで遊ぶ様子

遊んでいるときに色々役立ったVim関数をまとめたいと思う。

こちらも合わせてどうぞ。

www.rasukarusan.com

文字列幅を取得する

  • 文字数と同じ幅のfloating windowを生成したいときに利用

文字の幅や文字数を取得する関数として

  • strwidth()
  • strlen()
  • strdisplaywidth()
  • strlen(substitute('ほげほげ', '.', 'x', 'g'))

など色々あるが、マルチバイトの文字列などを考えなくて済むのはstrdisplaywidth()だった。
文字列とウィンドウはイコールではないことを知っておこう。

https://rasukarusan.github.io/blog-assets/neovim-floating-window-tips/strdisplaywidth.gif
現在行の文字列と同じ幅のfloating windowを生成

floating windowの位置、大きさを変更する

  • redrawがポイント。while文中などでは再描画が必要になる

floating windowの位置を変更するにはnvim_win_set_config()で新たにconfigをセットしてあげればよい。幅や高さを変えたいときはconfigをセットする必要はなく、nvim_win_set_width()nvim_win_set_height()を使えば良い。
ただ、移動をアニメーションっぽく見せるためにはconfigをセットした後にredrawsleepが必要になる。
redrawが無いと、関数を実行した後にvimが再描画されるため、移動後のfloating windowしか表示されない。
また、sleepがないと一瞬で描画が終わってしまうため、アニメーションっぽくならない。

下記は右下に1ずつ移動していくvimscript。

" floating windowを生成
let config = { 
    \'relative': 'editor',
    \ 'row': 10,
    \ 'col': 10,
    \ 'width': 10,
    \ 'height': 10,
    \ 'anchor': 'NW',
    \ 'style': 'minimal',
    \}
let buf = nvim_create_buf(v:false, v:true)
let win_id = nvim_open_win(buf, v:false, config)

" floating windowを移動させる
let i = 0
while i < 10
    let newConfig = {
        \'relative': config.relative, 
        \'row': config.row + i, 
        \'col': config.col + i
        \}
    call nvim_win_set_config(win_id, newConfig)
    let i += 1
    redraw
    sleep 100ms
endwhile

https://rasukarusan.github.io/blog-assets/neovim-floating-window-tips/strdisplaywidth.gif
右下に1ずつ移動していく

floating windowの背景色をランダムな色でセットする

  • 複数のfloating windowを生成するときに区別できるとデバッグがしやすい

これはもうちょっとスマートに書けると思うが、デバッグ用なのでまあいいかなと甘んじてる。
1桁目(%x)と残り5桁(%05x)で分けているのは、そうした方が似た色が出にくいから。

" 60000はざっくり計算。00000~fffffの10進数以内だったら何でもいい。
let color = '#' . printf("%x", Random(16)) . printf("%05x", Random(60000))
let hl_name = 'ClipBG' . i
execute 'hi' hl_name 'guifg=#ffffff' 'guibg=' . color
call nvim_win_set_option(win_id, 'winhighlight', 'Normal:'.hl_name)


" 指定した整数内で乱数を生成
function Random(max) abort
  return str2nr(matchstr(reltimestr(reltime()), '\v\.@<=\d+')[1:]) % a:max
endfunction

https://rasukarusan.github.io/blog-assets/neovim-floating-window-tips/strdisplaywidth.gif
複数のfloating windowを区別するために色付けする

参考にさせて頂いたサイト

Draw color wheel in NeoVim · GitHub
Native Vim Random number script - Stack Overflow

window_idからwindow_numを取得する

これ単体だとあまり使わないが、後述する「指定のfloating windowにコマンドを実行する」ときに必要となる。

function! s:winid2tabnr(win_id) abort
  return win_id2tabwin(a:win_id)[1]
endfunction

このコードは下記のサイトから頂きました。

koturn.hatenablog.com

ここのサイトめちゃくちゃ綺麗にまとまっていて、かつほぼ全ての相互変換を網羅してるので本当に助かりました、感謝です。

指定したfloating windowにコマンドを実行する

  • 指定のfloating windowにexecuteしたいときに必要となる

floating windowでガッツリ遊ぼうと思ったら、特定のfloating windowにコマンドを実行したいってことが結構ある。 そんなときはwindoを使う。

" :help windo
:{window number}windo {cmd}

例えばメインウィンドウにフォーカスを戻したいなら:0windo :となる。
0番は最初に開いたウィンドウとなるのでおそらくメインウィンドウだろう。cmd:を渡しているのはShellScriptと同じ感覚で何もしないことを示す。 関数として作っておくと何かと便利(引数にwindow numberを渡してもいいと思う)

" メインウィンドウにattach
function! s:focus_to_main_window()
    execute "0windo :"
endfunction

また、これは前述のwindow_idからwindow_numberを取得するものと一緒に使うことが多い。 対象のfloating windowに文字列を挿入する、みたいなときはこうする。

" 指定したウィンドウIDのウィンドウの番号を得る
function! s:winid2tabnr(win_id) abort
  return win_id2tabwin(a:win_id)[1]
endfunction

" 指定したfloating windowの現在行に'hoge'を挿入
let win = s:winid2tabnr(win_id)
execute win . 'windo ' . "call setline('.', 'hoge')"

windoを連発するのが面倒な場合、下記のように一旦指定のfloating windowにフォーカスを移してから実行していくと楽。

let win = s:winid2tabnr(win_id)
" 指定のwindowにフォーカスを移す
execute win . 'windo :'
" フォーカスを移した先のwindowで実行される
call setline(".", "hoge")

メインウィンドウ以外のウィンドウを全て閉じる

Ctrl-w Ctrl-o

生成したwindowを全て閉じるときに便利。:lsを実行すると消えていることがわかる。ただし完全には削除されない。 :ls!を実行するとvim上には残っていることがわかる。
完全に消すには

:bw[N]

でNにバッファ番号を指定すればOK。

こちらのサイトを参考にさせて頂きました。

さわさわと vim バッファ削除 バッファ一括削除

floatをintに変換

  • floating windowのconfigでcolとrowがfloatだが、計算するときにはint型で欲しい

floating windowを移動させるときなど、ある程度計算を行った後にcolやrowに値をぶち込むことが多い。
その計算途中でfloatだと面倒なので必要になった。

" float型をint型に変換
let col = float2nr(config.col) + 10

終わり

所詮小さいScriptなので、なんとなく頭の中でこうやればできるなと思うことはできるが、いざ実装しようとすると何の関数を使えばいいのか、そもそもあるのかわからなくてめげそうになる。
ただ、Vimは先人様たちが必要だと思ったのは大体入っている。もしない場合は他で代替可能だという無言のメッセージだと捉えれば、楽しくVimScript開発できる。