これはVim Advent Calendar 2021の14日目の記事です。
NeovimにFloating Windowが実装されて以来、様々なプラグインが開発、リプレイスされてきました。 有名所でいうとgit-messenger.vimでしょうか。Floating Windowの良い使い方だなあと感動した覚えがあります。
今日は僕が今まで開発してきたプラグインの中で、Floating Windowを変態的に使ったものを紹介したいと思います。
はじめに
記事内に折りたたまれているコードはダブルクリックでコピーが可能で、
test.vim
として保存 ->:source %
-> Shift+T
で実行できるようになっています。
ぜひ手元のNeovimでお試しください。Neovim >=0.4 であれば動きます。
矩形選択&ペーストを可視化して直感化
矩形選択からのペーストをビジュアライズするプラグインです。
矩形選択した箇所をFloating Windowにしてブロック化、カーソル移動できるところが気持ちいいですね。
文字を持ち上げる感じがVimを3D化しているようでエモいですよね。
また、ペーストしたあとにgv
で再選択できるのが地味に良いところです。
テトリスペースト
コード▼
こちらはヤンクしてからShift+T
を実行してください。また、できるだけ画面下で実行すると上から降ってくるのがよく見えます。
function! s:move_floating_window(win_id, relative, row, col) let newConfig = { \ 'relative': a:relative, \ 'row': a:row, \ 'col': a:col, \} call nvim_win_set_config(a:win_id, newConfig) redraw endfunction function! s:create_window(config) let buf = nvim_create_buf(v:false, v:true) let win_id = nvim_open_win(buf, v:true, a:config) hi mycolor guifg=#ffffff guibg=#dd6900 call nvim_win_set_option(win_id, 'winhighlight', 'Normal:mycolor') call nvim_win_set_option(win_id, 'winblend', 40) return win_id endfunction function! s:transparency_window(win_id) let i = 0 while i <= 50 call nvim_win_set_option(a:win_id, 'winblend', i*2) let i += 1 " 毎回redrawするとカクつくため if i % 2 == 0 redraw endif endwhile endfunction function! s:get_col() " 行番号を非表示にしている場合は調整不要なので0を返す if &number == 0 return 0 endif " 行数を表示している場合、行数の桁数分調整する必要がある. e.g) max_line = 100の場合3(桁) " +1しているのは行番号表示用ウィンドウのスペース分 let max_line = line("$") return strlen(max_line) + 1 endfunction function! s:get_width() return strlen(@*) endfunction function! s:get_height() let contents = split(@*,'\n') return len(contents) endfunction function! s:paste_to_current_window(number_of_line) if a:number_of_line == 1 let @* = substitute(@*,"\n","","g") let @* = @* . "\n" endif execute 'normal p' endfunction " ペーストする内容を入れる(表示する)ための空行を挿入 function! s:insert_empty_line(row) let i = 0 while i < a:row call append(expand('.'), '') let i += 1 endwhile endfunction function! s:delete_empty_line(row) execute 'normal ' . a:row . 'j' execute 'normal ' . a:row . '"_dd' execute 'normal ' . a:row . 'k' endfunction function! s:main() let start_row = 10 let col = s:get_col() let width = s:get_width() let height = s:get_height() let config = { 'relative': 'editor', 'row': start_row, 'col': col, 'width': width, 'height': height, 'anchor': 'NW', 'style': 'minimal',} if width == 0 || height == 0 return endif let win_id = s:create_window(config) " floating windowにクリップボードの内容をペースト execute 'normal P' " フォーカスをカレントウィンドウに戻す execute "0windo " . ":" " floating windowを上から降らす let move_y = line(".") - line("w0") - start_row let i = 0 while i <= move_y call s:move_floating_window(win_id, config.relative, config.row + i + 1, config.col) sleep 10ms let i += 1 endwhile " ペースト内容を表示するための空行を挿入 call s:insert_empty_line(height) " floating windowを透明化 call s:transparency_window(win_id) " カレントウィンドウにクリップボードの内容をペースト call s:paste_to_current_window(height) " 事前に挿入した空行を削除 call s:delete_empty_line(height) " floating windowを削除 call nvim_win_close(win_id, v:true) endfunction nnoremap <silent> T :call <SID>main()<CR>
ペーストを上から降らしてかっこよくします。
winblend
で透明度を動的に変更することでスパーク感を演出しています。
また同じような理論でデリートもテトリスっぽくできます。こちらは選択した行の幅をstrdisplaywidth
で取得し、Floating Windowでブロック化して分割することでテトリスらしさを醸し出すことに成功しています。
コード▼
function! s:create_window(config) let buf = nvim_create_buf(v:false, v:true) let win_id = nvim_open_win(buf, v:true, a:config) return win_id endfunction function! s:move_floating_window(win_id, relative, row, col) let newConfig = {'relative': a:relative, 'row': a:row, 'col': a:col,} call nvim_win_set_config(a:win_id, newConfig) redraw endfunction function! s:focus_to_main_window() execute "0windo :" endfunction function! s:get_col() " when `set nonumber` not need adjustment if &number == 0 return 0 endif " not support over 1000 line file return 4 endfunction function! s:split_words() let words = split(getline('.'), '\zs') let result = [] let index = 0 let i = 0 let word = '' let split_num = 7 while i < len(words) let word = word . words[i] if i % (len(words)/split_num) == 0 && i != 0 call insert(result, word, index) let word = '' let index += 1 endif let i += 1 endwhile call insert(result, word, index) return result endfunction function! s:fall_window(win_id) let move_y = line('w$') - line('.') let config = nvim_win_get_config(a:win_id) for y in range(0, move_y) call s:move_floating_window(a:win_id, config.relative, config.row + y + 1, config.col) sleep 4ms endfor endfunction function! s:set_color_random(win_id) let color = '#' . printf("%x", Random(16)) . printf("%05x", Random(69905)) let hl_name = 'ClipBG' . a:win_id execute 'hi' hl_name 'guifg=#ffffff' 'guibg=' . color call nvim_win_set_option(a:win_id, 'winhighlight', 'Normal:'.hl_name) endfunction function! s:create_words_window() let row = line('.') - line('w0') let col = s:get_col() let win_ids = [] let words = s:split_words() for word in words let width = strdisplaywidth(word) if width == 0 continue endif let config = { \'relative': 'editor', \ 'row': row, \ 'col': col, \ 'width': width, \ 'height': 1, \ 'anchor': 'NW', \ 'style': 'minimal', \} let win_id = s:create_window(config) call add(win_ids, win_id) call s:set_color_random(win_id) " call nvim_win_set_option(win_id, 'winblend', 100) call setline('.', word) call s:focus_to_main_window() let col += width endfor return win_ids endfunction function Random(max) abort return str2nr(matchstr(reltimestr(reltime()), '\v\.@<=\d+')[1:]) % a:max endfunction function! s:main() abort let win_ids = s:create_words_window() " fall current line call setline('.', '') for win_id in win_ids call s:fall_window(win_id) endfor execute 'normal dd' " close each floating window for win_id in win_ids call nvim_win_close(win_id, v:true) endfor endfunction call s:focus_to_main_window() nnoremap <silent> T :call <SID>main()<CR>
ボタンを作って発射する
ENTERでボタンPUSH&弾発射
コード▼
let g:button_window = {} hi FrontColor guibg=#F27200 hi BackColor guibg=#AC5D24 function! s:center(str) let width = nvim_win_get_width(0) let shift = floor(width/2) - floor(strdisplaywidth(a:str)/2) return repeat(' ', float2nr(shift)) . a:str endfunction function! s:move(direction, value) let front_win = nvim_get_current_win() let back_win = g:button_window[front_win] for id in [front_win, back_win] let config = nvim_win_get_config(id) if a:direction == 'x' let config.col += a:value else let config.row += a:value endif call nvim_win_set_config(id, config) endfor endfunction function! s:remove_button() abort let front_win = nvim_get_current_win() let back_win = g:button_window[front_win] call nvim_win_close(back_win, v:true) call nvim_win_close(front_win, v:true) call remove(g:button_window, front_win) endfunction function! s:create_window(config, ...) abort let hi_group = a:1 let transparency = get(a:, '2', 0) let buf = nvim_create_buf(v:false, v:true) let win = nvim_open_win(buf, v:true, a:config) if hi_group != '' call nvim_win_set_option(win, 'winhighlight', hi_group) call nvim_win_set_option(win, 'winblend', transparency) call nvim_win_set_config(win, a:config) endif return win endfunction function! s:fire() abort let front_win = nvim_get_current_win() let conf = nvim_win_get_config(front_win) let row = conf.row + 1 let col = conf.col + conf.width let width = 2 let height = 1 let config = { 'relative': 'editor', 'row': row, 'col': col, 'width': width, 'height': height, 'anchor': 'NW', 'style': 'minimal', } let ballet = s:create_window(config, 'Normal:FrontColor') for i in range(1, 30) let config.col += 1 call nvim_win_set_config(ballet, config) redraw sleep 10ms endfor call nvim_win_close(ballet, v:true) endfunction function! s:push() abort let front_win = nvim_get_current_win() let back_win = g:button_window[front_win] let config = nvim_win_get_config(front_win) let config.row += 1 call nvim_win_set_config(front_win, config) sleep 100ms redraw let config.row -= 1 call nvim_win_set_config(front_win, config) call s:fire() endfunction function! s:main() abort let row = 20 let col = 20 let width = 20 let height = 3 let config = { 'relative': 'editor', 'row': row, 'col': col, 'width': width, 'height': height, 'anchor': 'NW', 'style': 'minimal' } " 影の部分 let back_config = deepcopy(config) let back_config.row += 1 let back_win = s:create_window(back_config, 'Normal:BackColor') " 前面の部分 let front_win = s:create_window(config, 'Normal:FrontColor') call setline(2, s:center('Button')) call cursor(2, 0) nnoremap <buffer><nowait><silent> l :call <SID>move('x', 2)<CR> nnoremap <buffer><nowait><silent> h :call <SID>move('x', -2)<CR> nnoremap <buffer><nowait><silent> j :call <SID>move('y', 2)<CR> nnoremap <buffer><nowait><silent> k :call <SID>move('y', -2)<CR> nnoremap <buffer><nowait><silent> :q :call <SID>remove_button() nnoremap <CR> :call <SID>push()<CR> " 影と前面のウィンドウを紐付け let g:button_window[front_win] = back_win endfunction call s:main()
Floating Windowではボタンも作れます。さらには弾を発射することも可能です。
ボタンに見せるには全面と背面で2枚のFloating Windowを利用し、背面の色をちょっと暗くするとボタンに見えます。この辺はVimというよりデザインの話ですね。デザイナーもVimをやる時代です。
行飛ばしで選択してコピー
飛ばし飛ばしに行を選択してコピー
Vimでは飛ばし飛ばしの行を選択してコピーや削除するといったことが簡単にはできないので作りました。
ただ、実を言うとこれはFloating Windowを使っていません。「ExtMark」というNeovimの別の機能を使っています。
当初はFloating Windowで実装していたのですが、ExtMarkを使ったほうが動作が軽くなり、画面をまたぐ移動選択も可能になったためリプレイスしました。
ExtMarkもFloating Windowと同じぐらい変態的な使い方がまだまだ隠されていると思っているので、紹介させていただきました。
Vimでビートマニア(@skanehiraさん作)
引用元: Vimでビートマニアする
最後にこちらは他の方が作られたプラグインですが、「ああなかなかに変態だな...」と感じたため紹介させていただきます。作られたのはゴリラ界で最も有名なVimmerである@skanehiraさん、通称gorillaさんですね。
こちらはFloating WindowではなくVimのpopup window
を利用されているようです。timer_start
を利用した非同期処理はとても参考になります。
最後に
Floating Windowのいろいろな使い方を紹介しました。まだまだこれら以外にもマニアックな使い方があると思います。
こんなプラグインもあるよという方、ぜひコメント欄にて教えてほしいです。また、これからプラグインを作る方にとって何かしらの刺激になっていたら幸いです。
これからもVimで最高に遊んでいきたいですね。ではまた。