FloatingWindowには枠線をつけるオプションがない
border:1px
のようなオプションは用意されていない。ただ、枠線があるように見せることは可能。
下記はこちらの記事で作成したときのもの。カレンダーを表現するのに枠線付きのFloatingWindowが必要だった。ソース。
カレンダーを枠線FloatingWindowで表現
枠線付きのFloatingWindowを作成する
枠線用にもう一枚FloatingWindowを作り、同じ位置にFloatingWindowを重ねることで枠線付きのFloatingWindowを表現する。
" コンテンツ用ウィンドウの作成 function! s:create_contents_window(config) abort let config = {'relative': 'editor', 'row': a:config.row + 1, 'col': a:config.col + 2, 'width': a:config.width - 4, 'height': a:config.height - 2, 'style': 'minimal'} let buffer = nvim_create_buf(v:false, v:true) return nvim_open_win(buffer, v:true, config) endfunction " 枠線用ウィンドウの作成 function! s:create_border_window(config) abort let width = a:config.width let height = a:config.height let top = "╭" . repeat("─", width - 2) . "╮" let mid = "│" . repeat(" ", width - 2) . "│" let bot = "╰" . repeat("─", width - 2) . "╯" let lines = [top] + repeat([mid], height - 2) + [bot] let buffer = nvim_create_buf(v:false, v:true) call nvim_buf_set_lines(buffer, 0, -1, v:true, lines) return nvim_open_win(buffer, v:true, a:config) endfunction " 2つで1つのウィンドウとしてみせる function! s:new_window(config) abort call s:create_border_window(a:config) call s:create_contents_window(a:config) endfunction " ex.) 使い方 let config = { 'relative': 'editor', 'row': 30, 'col': 30, 'width': 50, 'height': 20, 'anchor': 'NW', 'style': 'minimal'} call s:new_window(config)
参考: Feature: optional floating window borders
FloatingWindowにコンテンツを載せる
nvim_buf_set_lines()
を使うと配列で1行ずつセットできるので直感的。下記ではfield
という配列を用意し、一行ずつコンテンツ内容を記載している。
let field = \ ['今日の日付'] \ + [''] \ + ['2021/01/31'] call nvim_buf_set_lines(buffer, 0, -1, v:true, field)
" コンテンツ用ウィンドウの作成 function! s:create_contents_window(config, field) abort let config = {'relative': 'editor', 'row': a:config.row + 1, 'col': a:config.col + 2, 'width': a:config.width - 4, 'height': a:config.height - 2, 'style': 'minimal'} let buffer = nvim_create_buf(v:false, v:true) call nvim_buf_set_lines(buffer, 0, -1, v:true, a:field) return nvim_open_win(buffer, v:true, config) endfunction " 2つで1つのウィンドウとしてみせる function! s:new_window(config, field) abort call s:create_border_window(a:config) call s:create_contents_window(a:config, a:field) endfunction let config = { 'relative': 'editor', 'row': 30, 'col': 30, 'width': 50, 'height': 20, 'anchor': 'NW', 'style': 'minimal'} let field = \ ['今日の日付'] \ + [''] \ + ['2021/01/31'] call s:new_window(config, field)
:qで閉じるとき、2回実行する必要がある
追記:
:q
を上書きしなくても、autocmd BufWinLeave
を利用すればシンプルに削除処理を登録できる。
下記をコンテンツウィンドウに設定すればOK。
" ウィンドウ削除処理を、コンテンツ用ウィンドウに登録 call nvim_command('autocmd BufWinLeave <buffer> call s:close_window()') " 下記が:qを上書きするスタイル。BufWinLeaveを使ったほうが美しい。 " nnoremap <buffer><nowait><silent> :q<CR> :call <SID>close_window()<CR>
参考:https://www.2n.pl/blog/how-to-write-neovim-plugins-in-lua.md
追記ここまで
枠線用とコンテンツ用の2つのFloatingWindowがあるので、完全に閉じるには2回:q
を実行しないといけない。
ただ、これはちょっと面倒くさいので1回の:q
で両方とも閉じられるようにする。要はコンテンツ用ウィンドウのIDと枠線用ウィンドウのIDを紐付けしておけばいいので色々やり方はある。今回はディクショナリ型を使って実装してみる。
" 枠線用ウィンドウとコンテンツウィンドウを紐付ける連想配列 let s:window_ids = {} " コンテンツ・枠線のウィンドウを削除 function! s:close_window(...) let contents_window_id = a:0 == 0 ? nvim_get_current_win() : a:1 let border_window_id = get(s:window_ids, contents_window_id, -1) if border_window_id != -1 call nvim_win_close(contents_window_id, v:true) call nvim_win_close(border_window_id, v:true) call remove(s:window_ids, contents_window_id) endif endfunction " 2つで1つのウィンドウとしてみせる function! s:new_window(config, field) abort let border_window_id = s:create_border_window(a:config) let contents_window_id = s:create_contents_window(a:config, a:field) " コンテンツ用ウィンドウと枠線用ウィンドウを紐付ける let s:window_ids[contents_window_id] = border_window_id " ウィンドウ削除処理を、コンテンツ用ウィンドウに登録 nnoremap <buffer><nowait><silent> :q<CR> :call <SID>close_window()<CR> return contents_window_id endfunction let config = { 'relative': 'editor', 'row': 30, 'col': 30, 'width': 50, 'height': 20, 'anchor': 'NW', 'style': 'minimal'} let field = \ ['今日の日付'] \ + [''] \ + ['2021/01/31'] call s:new_window(config, field)
:q
をnnoremap
で上書きするスタイル。<buffer>
オプションをつけているので、元のウィンドウにはキーバインドが登録されず、FloatingWindowにだけ登録される。s:close_window(...)
では引数を取れるようにしてある。こうすることでコンテンツウィンドウにフォーカスが乗っていなくても、IDさえ知っておけばどこからでも削除できる。
" どこからでも削除できるようTに削除処理を登録、みたいなことができる let contents_window_id = s:new_window(config, field) nnoremap T :call <SID>close_window(contents_window_id)<CR>
移動できるようにする
hjkl
で作成したウィンドウを移動できるようにしてみる。
function! s:move(direction, value) let contents_window_id = nvim_get_current_win() let border_window_id = get(s:window_ids, contents_window_id, -1) for id in [contents_window_id, border_window_id] 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 " 2つで1つのウィンドウとしてみせる function! s:new_window(config, field) abort let border_window_id = s:create_border_window(a:config) let contents_window_id = s:create_contents_window(a:config, a:field) " コンテンツ用ウィンドウと枠線用ウィンドウを紐付ける let s:window_ids[contents_window_id] = border_window_id " ウィンドウ削除処理を、コンテンツ用ウィンドウに登録 nnoremap <buffer><nowait><silent> :q<CR> :call <SID>close_window()<CR> " 移動操作を登録 nnoremap <buffer><nowait><silent> l :call <SID>move('x', 1)<CR> nnoremap <buffer><nowait><silent> h :call <SID>move('x', -1)<CR> nnoremap <buffer><nowait><silent> j :call <SID>move('y', 1)<CR> nnoremap <buffer><nowait><silent> k :call <SID>move('y', -1)<CR> return contents_window_id endfunction
hjlkでFloatingWindowを移動。ちょっとおもしろい。
これを応用したvim-block-paste
というプラグインを以前作った。
最終的なソース
" 枠線用ウィンドウとコンテンツウィンドウを紐付ける連想配列 let s:window_ids = {} " コンテンツ用ウィンドウの作成 function! s:create_contents_window(config, field) abort let config = {'relative': 'editor', 'row': a:config.row + 1, 'col': a:config.col + 2, 'width': a:config.width - 4, 'height': a:config.height - 2, 'style': 'minimal'} let buffer = nvim_create_buf(v:false, v:true) call nvim_buf_set_lines(buffer, 0, -1, v:true, a:field) return nvim_open_win(buffer, v:true, config) endfunction " 枠線用ウィンドウの作成 function! s:create_border_window(config) abort let width = a:config.width let height = a:config.height let top = "╭" . repeat("─", width - 2) . "╮" let mid = "│" . repeat(" ", width - 2) . "│" let bot = "╰" . repeat("─", width - 2) . "╯" let lines = [top] + repeat([mid], height - 2) + [bot] let buffer = nvim_create_buf(v:false, v:true) call nvim_buf_set_lines(buffer, 0, -1, v:true, lines) return nvim_open_win(buffer, v:true, a:config) endfunction function! s:move(direction, value) let contents_window_id = nvim_get_current_win() let border_window_id = get(s:window_ids, contents_window_id, -1) for id in [contents_window_id, border_window_id] 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 " コンテンツ・枠線のウィンドウを削除 " コンテンツウィンドウIDを引数で指定できるようにしておくと便利 function! s:close_window(...) let contents_window_id = a:0 == 0 ? nvim_get_current_win() : a:1 let border_window_id = get(s:window_ids, contents_window_id, -1) if border_window_id != -1 call nvim_win_close(contents_window_id, v:true) call nvim_win_close(border_window_id, v:true) call remove(s:window_ids, contents_window_id) endif endfunction " 2つで1つのウィンドウとしてみせる function! s:new_window(config, field) abort let border_window_id = s:create_border_window(a:config) let contents_window_id = s:create_contents_window(a:config, a:field) " コンテンツ用ウィンドウと枠線用ウィンドウを紐付ける let s:window_ids[contents_window_id] = border_window_id " ウィンドウ削除処理を、コンテンツ用ウィンドウに登録 nnoremap <buffer><nowait><silent> :q<CR> :call <SID>close_window()<CR> nnoremap <buffer><nowait><silent> j :call <SID>move('y', 1)<CR> nnoremap <buffer><nowait><silent> k :call <SID>move('y', -1)<CR> nnoremap <buffer><nowait><silent> l :call <SID>move('x', 1)<CR> nnoremap <buffer><nowait><silent> h :call <SID>move('x', -1)<CR> return contents_window_id endfunction let config = { 'relative': 'editor', 'row': 30, 'col': 30, 'width': 50, 'height': 20, 'anchor': 'NW', 'style': 'minimal'} let field = \ ['今日の日付'] \ + [''] \ + ['2021/01/31'] let contents_window_id = s:new_window(config, field) nnoremap T :call <SID>close_window(contents_window_id)<CR>
終わり
hjklで移動できると楽しいのでぜひやってみてほしい。