Eric's Blog 时光荏苒,岁月如梭

我的 Neovim 之旅

2023-05-24
Eric Wong

初始 Neovim

在接触 Neovim 之前,我的主力编辑器一直是 Vim。在 Vim 增加 +job 特性之前, Vim 的所有函数都是在主线程内完成的,当执行某个需要很长时间的操作时,Vim 会卡住等待上一个操作完成。 这样的体验是非常不符合个人习惯的。因此,Geoff Greer 给 Vim 提交过异步执行的补丁,源码可以查阅Floobits/vim。同时,Thiago Padilha 也给 Vim 提交过类似的补丁,但是都因各种原因被拒绝,其实这也是我不太喜欢的一个 Vim 开发的弊端。

后来,Thiago Padilha 发起了 Neovim 项目,做了如下改进:

  1. 增加异步 job 特性
  2. 悬浮窗口支持
  3. 增加 timer 支持
  4. 增加终端真色支持
  5. 增加 RPC 通讯支持
  6. 增加内置终端模拟器

虽然,以上的一些功能 Vim 后续也增加了,但是部分功能却是用了另外一种完全不兼容的方式。

Neovim 异步 job 特性

Neovim 的异步 job 特性主要是提供了一个异步执行外部命令的函数。并且通过回调函数来响应输出的结果。比如:

call jobstart(['echo'])

Neovim 定时器

Neovim 增加了一个定时器功能,主要涉及函数为 timer_start(),其函数原型为:

timer_start({time}, {callback} [, {options}])

悬浮窗口支持

以往 Vim 支持垂直、水平分屏,而分屏时窗口的布局、屏幕内容都会发生改变。悬浮窗口提供了一种在不改变当前窗口布局的前提下,打开额外的窗口以展示新的内容。

Neovim 和 Vim 的分屏又是完全不一样的函数。因此我写了一个函数,来统一调用 Neovim 或者 Vim 的悬浮窗口。

if has('nvim')
  let s:FLOAT = SpaceVim#api#import('neovim#floating')
else
  let s:FLOAT = SpaceVim#api#import('vim#floating')
endif
call s:FLOAT.open_win(bufnr('%'), v:true,
  \ {
  \ 'relative': 'editor',
  \ 'width'   : &columns,
  \ 'height'  : &lines * 30 / 100,
  \ 'row': 0,
  \ 'col': &lines - (&lines * 30 / 100) - 2
  \ })

Neovim 内置终端的使用

可以通过 :terminal 命令打开内置终端。在终端窗口内,可以使用 Ctrl-\ Ctrl-n 切换到 Normal 模式。

目前,内置的:terminal命令不支持分屏模式,可以借助split-term.vim插件。 该插件增加了如下的使用方式:

  1. 水平/垂直分屏打开终端
  2. 指定分屏终端窗口初始化大小
  3. 在新的标签页打开终端

优化内置终端的插件有很多种:

  • https://github.com/voldikss/vim-floaterm
  • https://github.com/numToStr/FTerm.nvim

虽然这些插件都挺不错的,但是个人感觉没有必要那么复杂。目前,就个人而言,我仅仅使用了 Neovim 的悬浮窗口配合 termopen 函数。

shell.vim#L153-L292

let s:SYSTEM = SpaceVim#api#import('system')
let s:FLOAT = SpaceVim#api#import('neovim#floating')
let s:WIN = SpaceVim#api#import('vim#window')
function! s:open_default_shell(open_with_file_cwd) abort
  if a:open_with_file_cwd
    if getwinvar(winnr(), '&buftype') ==# 'terminal'
      let path = getbufvar(winbufnr(winnr()), '_spacevim_shell_cwd', SpaceVim#plugins#projectmanager#current_root())
    else
      let path = expand('%:p:h')
    endif
  else
    let path = SpaceVim#plugins#projectmanager#current_root()
    " if the current file is not in a project, the projectmanager return empty
    " string. Then use current directory as default cwd.
    if empty(path)
      let path  = getcwd()
    endif
  endif

  " look for already opened terminal windows
  let windows = []
  windo call add(windows, winnr())
  for window in windows
    if getwinvar(window, '&buftype') ==# 'terminal'
      exe window .  'wincmd w'
      if getbufvar(winbufnr(window), '_spacevim_shell_cwd') ==# l:path
        " startinsert do not work in gvim
        if has('nvim')
          startinsert
        else
          normal! a
        endif
        return
      else
        " the opened terminal window is not the one we want.
        " close it, we're gonna open a new terminal window with the given l:path
        exe 'wincmd c'
        break
      endif
    endif
  endfor

  if s:default_position ==# 'float' && exists('*nvim_open_win')
    let s:term_win_id =  s:FLOAT.open_win(bufnr('%'), v:true,
          \ {
            \ 'relative': 'editor',
            \ 'width'   : &columns,
            \ 'height'  : &lines * s:default_height / 100,
            \ 'row': 0,
            \ 'col': &lines - (&lines * s:default_height / 100) - 2
            \ })

    exe win_id2win(s:term_win_id) .  'wincmd w'
  else
    " no terminal window found. Open a new window
    let cmd = s:default_position ==# 'float' ?
          \ 'topleft split' :
          \ s:default_position ==# 'top' ?
          \ 'topleft split' :
          \ s:default_position ==# 'bottom' ?
          \ 'botright split' :
          \ s:default_position ==# 'right' ?
          \ 'rightbelow vsplit' : 'leftabove vsplit'
    exe cmd
    let lines = &lines * s:default_height / 100
    if lines < winheight(0) && (s:default_position ==# 'top' || s:default_position ==# 'bottom')
      exe 'resize ' . lines
    endif
  endif
  let w:shell_layer_win = 1
  for open_terminal in s:open_terminals_buffers
    if bufexists(open_terminal)
      if getbufvar(open_terminal, '_spacevim_shell_cwd') ==# l:path
        exe 'silent b' . open_terminal
        " clear the message
        if has('nvim')
          startinsert
        else
          normal! a
        endif
        return
      endif
    else
      " remove closed buffer from list
      call remove(s:open_terminals_buffers, 0)
    endif
  endfor

  " no terminal window with l:path as cwd has been found, let's open one
  if s:default_shell ==# 'terminal'
    if exists(':terminal')
      if has('nvim')
        if s:SYSTEM.isWindows
          let shell = empty($SHELL) ? 'cmd.exe' : $SHELL
        else
          let shell = empty($SHELL) ? 'bash' : $SHELL
        endif
        enew
        call termopen(shell, {'cwd': l:path})
        " @bug cursor is not cleared when open terminal windows.
        " in neovim-qt when using :terminal to open a shell windows, the orgin
        " cursor position will be highlighted. switch to normal mode and back
        " is to clear the highlight.
        " This seem a bug of neovim-qt in windows.
        "
        " cc @equalsraf
        if s:SYSTEM.isWindows && has('nvim')
          stopinsert
          startinsert
        endif
        let s:term_buf_nr = bufnr('%')
        call extend(s:shell_cached_br, {getcwd() : s:term_buf_nr})
      else
        " handle vim terminal
        if s:SYSTEM.isWindows
          let shell = empty($SHELL) ? 'cmd.exe' : $SHELL
        else
          let shell = empty($SHELL) ? 'bash' : $SHELL
        endif
        let s:term_buf_nr = term_start(shell, {'cwd': l:path, 'curwin' : 1, 'term_finish' : 'close'})
      endif
      call add(s:open_terminals_buffers, s:term_buf_nr)
      let b:_spacevim_shell = shell
      let b:_spacevim_shell_cwd = l:path

      " use WinEnter autocmd to update statusline
      doautocmd WinEnter
      setlocal nobuflisted nonumber norelativenumber

      " use q to hide terminal buffer in vim, if vimcompatible mode is not
      " enabled, and smart quit is on.
      if !empty(g:spacevim_windows_smartclose)  && !g:spacevim_vimcompatible
        exe 'nnoremap <buffer><silent> ' . g:spacevim_windows_smartclose . ' :hide<CR>'
      endif
      startinsert
    else
      echo ':terminal is not supported in this version'
    endif
  elseif s:default_shell ==# 'VimShell'
    VimShell
    imap <buffer> <C-d> exit<esc><Plug>(vimshell_enter)
  endif
endfunction

延生阅读

分享到:

评论

目前只支持使用邮件参与评论。