これはVim Advent Calendar 2021 の14日目の記事です。
NeovimにFloating Windowが実装されて以来、様々なプラグインが開発、リプレイスされてきました。
有名所でいうとgit-messenger.vim でしょうか。Floating Windowの良い使い方だなあと感動した覚えがあります。
今日は僕が今まで開発してきたプラグインの中で、Floating Windowを変態的に使ったものを紹介したいと思います。
はじめに
記事内に折りたたまれているコードはダブルクリックでコピーが可能で、
test.vim
として保存 ->:source %
-> Shift+T
で実行できるようになっています。
ぜひ手元のNeovimでお試しください。Neovim >=0.4 であれば動きます。
矩形選択&ペーストを可視化して直感化
github.com
矩形選択からのペーストをビジュアライズするプラグインです。
矩形選択した箇所を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
if i % 2 == 0
redraw
endif
endwhile
endfunction
function ! s: get_col()
if &number == 0
return 0
endif
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)
execute 'normal P'
execute "0windo " . ":"
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 10 ms
let i += 1
endwhile
call s:insert_empty_line( height)
call s:transparency_window( win_id)
call s:paste_to_current_window( height)
call s:delete_empty_line( height)
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()
if &number == 0
return 0
endif
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 4 ms
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 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()
call setline ( '.' , '' )
for win_id in win_ids
call s:fall_window( win_id)
endfor
execute 'normal dd'
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>
ボタンを作って発射する
コード▼
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 10 ms
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 100 ms
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をやる時代です。
行飛ばしで選択してコピー
github.com
Vimでは飛ばし飛ばしの行を選択してコピーや削除するといったことが簡単にはできないので作りました。
ただ、実を言うとこれはFloating Windowを使っていません。「ExtMark」というNeovimの別の機能を使っています。
当初はFloating Windowで実装していたのですが、ExtMarkを使ったほうが動作が軽くなり、画面をまたぐ移動選択も可能になったためリプレイスしました。
ExtMarkもFloating Windowと同じぐらい変態的な使い方がまだまだ隠されていると思っているので、紹介させていただきました。
Vimでビートマニア(@skanehiraさん作)
Vimでビートマニア
引用元: Vimでビートマニアする
github.com
最後にこちらは他の方が作られたプラグインですが、「ああなかなかに変態だな...」と感じたため紹介させていただきます。作られたのはゴリラ界で最も有名なVimmerである@skanehira さん、通称gorillaさんですね 。
こちらはFloating WindowではなくVimのpopup window
を利用されているようです。timer_start
を利用した非同期処理はとても参考になります。
最後に
Floating Windowのいろいろな使い方を紹介しました。まだまだこれら以外にもマニアックな使い方があると思います。
こんなプラグインもあるよという方、ぜひコメント欄にて教えてほしいです。また、これからプラグインを作る方にとって何かしらの刺激になっていたら幸いです。
これからもVimで最高に遊んでいきたいですね。ではまた。