首发于黑客随笔
neovim 悬浮窗口介绍

neovim 悬浮窗口介绍

前言

时间来到2017年4月30号,一名叫做bfredl的网友向neovim仓库提交了一个pr

[RFC] Floating windows in TUI and Remote UI by bfredl · Pull Request #6619 · neovim/neovim


这个pr实现了不少ide都有的一个功能那就是悬浮窗口,就如文章封面图的效果,看到这个效果你甚至可以联想到未来是不是能实现类似visual studio或者eclipse类似的效果:


时隔将近两年,这个pr终于被合并了。


如何判断自己的正在用的neovim是否支持这个特性呢,打开neovim,然后输入命令


:h nvim_open_win
:h nvim_win_set_config


如果能顺利找到这两个函数的help文档,那么恭喜你,你的neovim已经支持这个特性了,如果没有那么你最好在网上找找如何安装最新neovim的方式。


毫无疑问,上面这两个函数就是悬浮窗口的核心函数,所谓授人以鱼不如授人以渔,所以这篇个文章不是向小白们show这个特性多么牛逼,而是教你如何为自己的neovim创建悬浮窗口。利用这个特性,你可以创造很多有趣的功能,文章的最后也会收集列出一些基于这个特性的neovim插件。

介绍

关于介绍悬浮窗口的入口文档,可以由下面的命令进入:

:h api-floatwin


悬浮窗口和普通vim窗口基本一样,你可以在悬浮窗口上编辑,所支持的选项和普通窗口几乎一样。

悬浮窗口由nvim_open_win()函数创建,或者我们可以使用nvim_win_set_config()函数将一个普通窗口转化成悬浮窗口。

悬浮窗口的位置(坐标)可以相对于当前窗口左上角也可以基于当前光标。这个设置可以在函数nvim_open_win()中可以指定。


nvim_open_win()函数创建一个悬浮窗口的前提是,这个窗口需要由一个关联的buffer,这个buffer可以由nvim_create_buffer()函数来创建。在这个buffer里面的文本可以通过api-highlights来进行高亮。


下面是创建一个悬浮窗口的示例代码:


    let buf = nvim_create_buf(v:false, v:true)
    call nvim_buf_set_lines(buf, 0, -1, v:true, ["test", "text"])
    let opts = {'relative': 'cursor', 'width': 10, 'height': 2, 'col': 0,
        \ 'row': 1, 'anchor': 'NW'}
    let win = nvim_open_win(buf, 0, opts)
    " optional: change highlight, otherwise Pmenu is used
    call nvim_win_set_option(win, 'winhl', 'Normal:MyHighlight')


你可以用nvim_win_close()函数来关闭悬浮窗口。

执行下面命令查看各个函数具体用法,当然下一章会根据实例来讲解。

:h nvim_open_win()
:h nvim_open_close()
:h nvim_create_buf()

动手创建一个你自己的悬浮窗口

悬浮窗口这个特性出来之后,我立刻想到的idea是操作系统的通知中心,从桌面右上弹出通知消息,是不是很酷?

现在就动手实现这个功能,首先本人的配置,经常会需要打印一些信息到cmd窗口中,比如当前路径,异步命令的执行状态等等,现在我们的目标就是把这些信息通过悬浮窗口显示出来,而且是从右上出来的。

后期,我们可以通过悬浮窗口,弹出任何我们需要的消息,比如整点报时,天气预报等。。当然这些还需要利用到neovim的异步特性,python接口等。这里就不细讲。

先看下效果


let s:win_list=[]
let s:global_echo_str=[]

function! NvimCloseWin(timer) abort
    call timer_info(a:timer)
    let l:flag=0
    try
        call nvim_win_close(s:win_list[0], v:true)
    catch
        call remove(s:win_list, 0)
        let l:flag=1
    endtry
    if !empty(s:win_list) && l:flag == 0
        call remove(s:win_list, 0)
    endif
endfunction

"echo warning messag
"a:1-->err or warn or info,default is warn
"a:2-->flag of VimEnter,0 or 1
function! te#utils#EchoWarning(str,...) abort
    let l:level='WarningMsg'
    let l:prompt='warn'
    let l:flag=0
    if a:0 != 0
        for s:needle in a:000
            if type(s:needle) == g:t_string
                let l:prompt = s:needle
                if s:needle ==? 'err'
                    let l:level='ErrorMsg'
                elseif s:needle ==? 'warn'
                    let l:level='WarningMsg'
                elseif s:needle ==? 'info'
                    let l:level='None'
                endif
            elseif type(s:needle) == g:t_number
                let l:flag=s:needle
            endif
        endfor
    endif
    if l:flag != 0 || has('vim_starting')
        call add(s:global_echo_str, a:str)
        return
    endif
    if te#env#IsNvim() && exists('*nvim_open_win') && exists('*nvim_win_set_config')
        let l:str='['.l:prompt.'] '.a:str
        let l:bufnr = nvim_create_buf(v:false, v:false)
        let l:opts = {'relative': 'editor', 'width': strlen(l:str)+3, 'height': 1, 'col': &columns,
                    \ 'row': 3+len(s:win_list), 'anchor': 'NW'}
        let l:win=nvim_open_win(l:bufnr, v:false,l:opts)
        call nvim_buf_set_lines(l:bufnr, 0, -1, v:false, [l:str])
        hi def NvimFloatingWindow  term=None guifg=black guibg=#f94e3e ctermfg=black ctermbg=210
        call nvim_win_set_option(l:win, 'winhl', 'Normal:NvimFloatingWindow')
        call nvim_win_set_option(l:win, 'number', v:false)
        call nvim_win_set_option(l:win, 'relativenumber', v:false)
        call nvim_buf_set_option(l:bufnr, 'buftype', 'nofile')
        call nvim_buf_set_option(l:bufnr, 'bufhidden', 'wipe')
        call nvim_buf_set_option(l:bufnr, 'modified', v:false)
        call nvim_buf_set_option(l:bufnr, 'buflisted', v:false)
        call add(s:win_list, l:win)
        call timer_start(5000, 'NvimCloseWin', {'repeat': 1})
    else
        redraw!
        execut 'echohl '.l:level | echom '['.l:prompt.'] '.a:str | echohl None
    endif
endfunction

核心代码如上,用户只要像下面这样调用就能调用悬浮窗口显示信息了:


:call te#utils#EchoWarning("Please install yapf or autopep8")

开始讲解代码,悬浮窗口的代码从nvim_create_buf函数开始,它创建了一个buffer,而nvim_open_win创建一个悬浮窗口并关联到这个buffer上面,其中l:opts是悬浮窗口的选项,选项各个含义如下:


- `relative` :如果设置那么窗口将变成悬浮窗口. 窗口的位置相对下面三个变量,
- "editor" 以编辑器窗口作为坐标系
- "win" a window. 以指定neovim窗口作为坐标系,默认当前窗口
- "cursor" :以当前光标作为坐标系

- `win` : 当 relative='win'时,对应的windows ID
- `anchor` : 定义悬浮窗口的行列的锚
- "NW" 西北 (默认)
- "NE" 东北
- "SW" 西南
- "SE" 东南

- `height` : window的高 (单位字符).最小是1
- `width` : window 宽 (单位字符)。最小是2
- `row` : 行坐标,单位和选项lines一致。可以是浮点型。
- `col` : 列坐标. 单位和选项columns一致。可以是浮点型。
- `focusable` : 悬浮窗口是否可以聚焦。是否可以通过wincmd来切换
- `external` : 指定外部gui窗口,当前不用这个选项。

值得注意的是,选项中height和width的设置比较零活的,既要考虑窗口布局,也要考虑多个消息之间不要重叠在一起。

nvim_open_win的第一个选项是buffer的handle,第二个选项是创建之后是否切换光标到悬浮窗口中,这里当然不要,所以选择v:false

nvim_buf_set_lines函数这里的作用就是将指定的字符串写到刚才申请的buffer里面,这里的字符串就是我们要显示的消息啦。

悬浮窗口的option默认是继承当前vim配置的,比如显示行数之类的,很明显我们不要显示行数,我们也希望这个悬浮窗口里面的buffer是不可编辑以及不可见的,这里就需要用到两个函数来设置选项

下面两个选项分别用于设置指定窗口和指定buffer的选项,如果某个选项是buffer指定的,则切记要用nvim_buf_set_option而不是nvim_win_set_option。

:h nvim_win_set_option
:h nvim_buf_set_option

在设置的选项中由一个比较特殊的,用于设置悬浮窗口的语法高亮,这里设置的是neovim的winhl选项,后面Normal:NvimFloatingWindow,给这个悬浮窗口指定了一个自定义高亮组,这个高亮组如下定义:

hi def NvimFloatingWindow  term=None guifg=black guibg=#f94e3e ctermfg=black ctermbg=210

显示完窗口之后,就要考虑什么时候关闭窗口,这个我们自然能想到的是,定时一段时间然后自动关闭。

这里用到neovim的另外一个特性就是定时器,用法很简单,一个函数创建一个定时器,时间一到自动调用回调函数,我们在timer的回调函数里面执行悬浮窗口关闭动作,如NvimCloseWin中所示。


流程就是这么简单,当然这里涉及到一些VimL的编程基础,我这样讲,有些小白还是一脸蒙bi的,没关系,现在开始学习,过一段时间之后,你会发现很简单。


支持悬浮窗口插件&配置

下面这个就是本文章截图所示的插件,显示git commit信息。

补全框架ncm2,现在也支持通过悬浮窗口来显示doc了。


字符搜索工具flygrep可以通过悬浮窗口实时显示搜索结果

补全框架coc,现在支持通过悬浮窗口来显示补全选项,文档等。


vim-which-key:vim版本的leader guide,现在支持通过悬浮窗口显示按键导航了。

查看和搜索LSP符合,支持悬浮窗口。

悬浮时钟:

本人配置:

编辑于 2019-04-01 10:30