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

如何正确地使用 ftplugin 目录

2025-12-23
Eric Wong

前面再阅读一些插件源码时,发现一个问题,很多插件的使用了 ftplugin 这个目录,其内的脚本文件中直接使用了 setlocal xx=xx 这样的语法。 在早期的 Neovim 或者 Vim 版本中这样确实没有问题,但是随着 Neovim 功能特性增加。这样写就会容易出错。 实际上,直到目前为止 Neovim 和 Vim 的官方文档 :h ftplugin 内的示例还是:

" Only do this when not done yet for this buffer
if exists("b:did_ftplugin")
  finish
endif
let b:did_ftplugin = 1
setlocal textwidth=70

Neovim 插件的 ftplugin 目录是一个特殊的文件夹,其内的文件会在 FileType 事件触发是被载入。

看一下 Neovim 的源码,ftplugin 目录下的文件是如何被载入的。

augroup filetypeplugin
  au FileType * call s:LoadFTPlugin()

  func! s:LoadFTPlugin()
    if exists("b:undo_ftplugin")
      exe b:undo_ftplugin
      unlet! b:undo_ftplugin b:did_ftplugin
    endif

    let s = expand("<amatch>")
    if s != ""
      if &cpo =~# "S" && exists("b:did_ftplugin")
	" In compatible mode options are reset to the global values, need to
	" set the local values also when a plugin was already used.
	unlet b:did_ftplugin
      endif

      " When there is a dot it is used to separate filetype names.  Thus for
      " "aaa.bbb" load "aaa" and then "bbb".
      for name in split(s, '\.')
        " Load Lua ftplugins after Vim ftplugins _per directory_
        " TODO(clason): use nvim__get_runtime when supports globs and modeline
        " XXX: "[.]" in the first pattern makes it a wildcard on Windows
        exe $'runtime! ftplugin/{name}[.] ftplugin/{name}_*. ftplugin/{name}/*.'
      endfor
    endif
  endfunc
augroup END

以上内容不难看出,Neovim 实际上是监听了 FileType 这个事件,然后根据 expand('<amatch>') 的值来执行 :runtime 命令。

但是,随着 Neovim 和 Vim 增加了设置非当前 buffer 的 option 这一功能后。就会出现这样问题,当 FileType 事件触发时,触发的 buffer 并非是当前 buffer。 那么在 ftplugin 内如果使用了 setlocal 这样的命令,有可能会设置错了缓冲区。

test_ft.lua

local log = require("logger").derive("ft")
log.info("nvim_get_current_buf() is " .. vim.api.nvim_get_current_buf())
log.info("nvim_get_current_win() is " .. vim.api.nvim_get_current_win())
log.info("-----------------------------------------------------")
local real_current_win = vim.api.nvim_get_current_win()
local newbuf = vim.api.nvim_create_buf(true, false)

local events = {}

for _, v in ipairs(vim.fn.getcompletion("", "event")) do
	if not vim.endswith(v, "Cmd") then
		table.insert(events, v)
	end
end

local id = vim.api.nvim_create_autocmd(events, {
	group = vim.api.nvim_create_augroup("test_ft", { clear = true }),
	pattern = { "*" },
	callback = function(ev)
		log.info("-----------------------------------------------------")
		log.info("event is " .. ev.event)
		log.info("ev.buf is " .. ev.buf)
		log.info("nvim_get_current_buf() is " .. vim.api.nvim_get_current_buf())
		log.info("nvim_get_current_win() is " .. vim.api.nvim_get_current_win())
		log.info("real_current_win's buf is" .. vim.api.nvim_win_get_buf(real_current_win))
	end,
})

vim.api.nvim_open_win(newbuf, false, { split = "above" })
vim.api.nvim_set_option_value("filetype", "test123", { buf = newbuf })
vim.api.nvim_del_autocmd(id)
log.info("-----------------------------------------------------")
log.info("nvim_get_current_buf() is " .. vim.api.nvim_get_current_buf())
log.info("nvim_get_current_win() is " .. vim.api.nvim_get_current_win())
[ 23:50:19:932 ] [ Info  ] [     ft ] nvim_get_current_buf() is 7
[ 23:50:19:932 ] [ Info  ] [     ft ] nvim_get_current_win() is 1000
[ 23:50:19:932 ] [ Info  ] [     ft ] -----------------------------------------------------
[ 23:50:19:933 ] [ Info  ] [     ft ] -----------------------------------------------------
[ 23:50:19:933 ] [ Info  ] [     ft ] event is WinNew
[ 23:50:19:933 ] [ Info  ] [     ft ] ev.buf is 7
[ 23:50:19:933 ] [ Info  ] [     ft ] nvim_get_current_buf() is 7
[ 23:50:19:933 ] [ Info  ] [     ft ] nvim_get_current_win() is 1008
[ 23:50:19:933 ] [ Info  ] [     ft ] real_current_win's buf is7
[ 23:50:19:934 ] [ Info  ] [     ft ] -----------------------------------------------------
[ 23:50:19:934 ] [ Info  ] [     ft ] event is BufWinEnter
[ 23:50:19:934 ] [ Info  ] [     ft ] ev.buf is 9
[ 23:50:19:934 ] [ Info  ] [     ft ] nvim_get_current_buf() is 9
[ 23:50:19:934 ] [ Info  ] [     ft ] nvim_get_current_win() is 1008
[ 23:50:19:934 ] [ Info  ] [     ft ] real_current_win's buf is7
[ 23:50:19:953 ] [ Info  ] [     ft ] -----------------------------------------------------
[ 23:50:19:953 ] [ Info  ] [     ft ] event is Syntax
[ 23:50:19:953 ] [ Info  ] [     ft ] ev.buf is 9
[ 23:50:19:953 ] [ Info  ] [     ft ] nvim_get_current_buf() is 9
[ 23:50:19:953 ] [ Info  ] [     ft ] nvim_get_current_win() is 1008
[ 23:50:19:953 ] [ Info  ] [     ft ] real_current_win's buf is7
[ 23:50:19:954 ] [ Info  ] [     ft ] -----------------------------------------------------
[ 23:50:19:954 ] [ Info  ] [     ft ] event is FileType
[ 23:50:19:954 ] [ Info  ] [     ft ] ev.buf is 9
[ 23:50:19:954 ] [ Info  ] [     ft ] nvim_get_current_buf() is 9
[ 23:50:19:954 ] [ Info  ] [     ft ] nvim_get_current_win() is 1008
[ 23:50:19:954 ] [ Info  ] [     ft ] real_current_win's buf is7
[ 23:50:19:954 ] [ Info  ] [     ft ] -----------------------------------------------------
[ 23:50:19:954 ] [ Info  ] [     ft ] event is OptionSet
[ 23:50:19:954 ] [ Info  ] [     ft ] ev.buf is 0
[ 23:50:19:954 ] [ Info  ] [     ft ] nvim_get_current_buf() is 9
[ 23:50:19:954 ] [ Info  ] [     ft ] nvim_get_current_win() is 1008
[ 23:50:19:954 ] [ Info  ] [     ft ] real_current_win's buf is7
[ 23:50:19:954 ] [ Info  ] [     ft ] -----------------------------------------------------
[ 23:50:19:954 ] [ Info  ] [     ft ] nvim_get_current_buf() is 7
[ 23:50:19:954 ] [ Info  ] [     ft ] nvim_get_current_win() is 1000

可以看到,在 event 触发 callback 函数内 nvim_get_current_win 和 nvim_get_current_buf 都临时被修改了。

测试一下,不开窗口效果呢?

local log = require("logger").derive("ft")
log.info("nvim_get_current_buf() is " .. vim.api.nvim_get_current_buf())
log.info("nvim_get_current_win() is " .. vim.api.nvim_get_current_win())
log.info("-----------------------------------------------------")
local real_current_win = vim.api.nvim_get_current_win()
local newbuf = vim.api.nvim_create_buf(true, false)

local events = {}

for _, v in ipairs(vim.fn.getcompletion("", "event")) do
	if not vim.endswith(v, "Cmd") then
		table.insert(events, v)
	end
end

local id = vim.api.nvim_create_autocmd(events, {
	group = vim.api.nvim_create_augroup("test_ft", { clear = true }),
	pattern = { "*" },
	callback = function(ev)
		log.info("-----------------------------------------------------")
		log.info("event is " .. ev.event)
		log.info("ev.buf is " .. ev.buf)
		log.info("nvim_get_current_buf() is " .. vim.api.nvim_get_current_buf())
		log.info("nvim_get_current_win() is " .. vim.api.nvim_get_current_win())
		log.info("real_current_win's buf is" .. vim.api.nvim_win_get_buf(real_current_win))
	end,
})

-- vim.api.nvim_open_win(newbuf, false, { split = "above" })
vim.api.nvim_set_option_value("filetype", "test123", { buf = newbuf })
vim.api.nvim_del_autocmd(id)
log.info("-----------------------------------------------------")
log.info("nvim_get_current_buf() is " .. vim.api.nvim_get_current_buf())
log.info("nvim_get_current_win() is " .. vim.api.nvim_get_current_win())
[ 23:53:49:058 ] [ Info  ] [     ft ] nvim_get_current_buf() is 10
[ 23:53:49:058 ] [ Info  ] [     ft ] nvim_get_current_win() is 1000
[ 23:53:49:058 ] [ Info  ] [     ft ] -----------------------------------------------------
[ 23:53:49:078 ] [ Info  ] [     ft ] -----------------------------------------------------
[ 23:53:49:078 ] [ Info  ] [     ft ] event is Syntax
[ 23:53:49:078 ] [ Info  ] [     ft ] ev.buf is 12
[ 23:53:49:078 ] [ Info  ] [     ft ] nvim_get_current_buf() is 12
[ 23:53:49:078 ] [ Info  ] [     ft ] nvim_get_current_win() is 1001
[ 23:53:49:078 ] [ Info  ] [     ft ] real_current_win's buf is10
[ 23:53:49:079 ] [ Info  ] [     ft ] -----------------------------------------------------
[ 23:53:49:079 ] [ Info  ] [     ft ] event is FileType
[ 23:53:49:079 ] [ Info  ] [     ft ] ev.buf is 12
[ 23:53:49:079 ] [ Info  ] [     ft ] nvim_get_current_buf() is 12
[ 23:53:49:079 ] [ Info  ] [     ft ] nvim_get_current_win() is 1001
[ 23:53:49:079 ] [ Info  ] [     ft ] real_current_win's buf is10
[ 23:53:49:079 ] [ Info  ] [     ft ] -----------------------------------------------------
[ 23:53:49:079 ] [ Info  ] [     ft ] event is OptionSet
[ 23:53:49:079 ] [ Info  ] [     ft ] ev.buf is 0
[ 23:53:49:079 ] [ Info  ] [     ft ] nvim_get_current_buf() is 12
[ 23:53:49:079 ] [ Info  ] [     ft ] nvim_get_current_win() is 1001
[ 23:53:49:079 ] [ Info  ] [     ft ] real_current_win's buf is10
[ 23:53:49:079 ] [ Info  ] [     ft ] -----------------------------------------------------
[ 23:53:49:079 ] [ Info  ] [     ft ] nvim_get_current_buf() is 10
[ 23:53:49:079 ] [ Info  ] [     ft ] nvim_get_current_win() is 1000

这窗口 1001 是什么鬼?临时隐藏窗口?

local log = require("logger").derive("ft")
log.info("nvim_get_current_buf() is " .. vim.api.nvim_get_current_buf())
log.info("nvim_get_current_win() is " .. vim.api.nvim_get_current_win())
log.info("-----------------------------------------------------")
local real_current_win = vim.api.nvim_get_current_win()
local newbuf = vim.api.nvim_create_buf(true, false)

local events = {}

for _, v in ipairs(vim.fn.getcompletion("", "event")) do
	if not vim.endswith(v, "Cmd") then
		table.insert(events, v)
	end
end

local id = vim.api.nvim_create_autocmd(events, {
	group = vim.api.nvim_create_augroup("test_ft", { clear = true }),
	pattern = { "*" },
	callback = function(ev)
		log.info("-----------------------------------------------------")
		log.info("event is " .. ev.event)
		log.info("ev.buf is " .. ev.buf)
		log.info("nvim_get_current_buf() is " .. vim.api.nvim_get_current_buf())
		log.info("nvim_get_current_win() is " .. vim.api.nvim_get_current_win())
        log.info('win count is ' .. vim.fn.winnr('$'))
        log.info('winconfig is ' .. vim.inspect(vim.api.nvim_win_get_config(vim.api.nvim_get_current_win())))
		log.info("real_current_win's buf is" .. vim.api.nvim_win_get_buf(real_current_win))
	end,
})

-- vim.api.nvim_open_win(newbuf, false, { split = "above" })
vim.api.nvim_set_option_value("filetype", "test123", { buf = newbuf })
vim.api.nvim_del_autocmd(id)
log.info("-----------------------------------------------------")
log.info("nvim_get_current_buf() is " .. vim.api.nvim_get_current_buf())
log.info("nvim_get_current_win() is " .. vim.api.nvim_get_current_win())
[ 23:57:49:249 ] [ Info  ] [     ft ] nvim_get_current_buf() is 9
[ 23:57:49:249 ] [ Info  ] [     ft ] nvim_get_current_win() is 1000
[ 23:57:49:249 ] [ Info  ] [     ft ] -----------------------------------------------------
[ 23:57:49:268 ] [ Info  ] [     ft ] -----------------------------------------------------
[ 23:57:49:268 ] [ Info  ] [     ft ] event is Syntax
[ 23:57:49:268 ] [ Info  ] [     ft ] ev.buf is 13
[ 23:57:49:268 ] [ Info  ] [     ft ] nvim_get_current_buf() is 13
[ 23:57:49:268 ] [ Info  ] [     ft ] nvim_get_current_win() is 1001
[ 23:57:49:268 ] [ Info  ] [     ft ] win count is 2
[ 23:57:49:268 ] [ Info  ] [     ft ] winconfig is {
  anchor = "NW",
  col = 0,
  external = false,
  focusable = false,
  height = 5,
  hide = false,
  mouse = false,
  relative = "editor",
  row = 0,
  width = 168,
  zindex = 50
}
[ 23:57:49:268 ] [ Info  ] [     ft ] real_current_win's buf is9
[ 23:57:49:269 ] [ Info  ] [     ft ] -----------------------------------------------------
[ 23:57:49:269 ] [ Info  ] [     ft ] event is FileType
[ 23:57:49:269 ] [ Info  ] [     ft ] ev.buf is 13
[ 23:57:49:269 ] [ Info  ] [     ft ] nvim_get_current_buf() is 13
[ 23:57:49:269 ] [ Info  ] [     ft ] nvim_get_current_win() is 1001
[ 23:57:49:269 ] [ Info  ] [     ft ] win count is 2
[ 23:57:49:270 ] [ Info  ] [     ft ] winconfig is {
  anchor = "NW",
  col = 0,
  external = false,
  focusable = false,
  height = 5,
  hide = false,
  mouse = false,
  relative = "editor",
  row = 0,
  width = 168,
  zindex = 50
}
[ 23:57:49:270 ] [ Info  ] [     ft ] real_current_win's buf is9
[ 23:57:49:270 ] [ Info  ] [     ft ] -----------------------------------------------------
[ 23:57:49:270 ] [ Info  ] [     ft ] event is OptionSet
[ 23:57:49:270 ] [ Info  ] [     ft ] ev.buf is 0
[ 23:57:49:270 ] [ Info  ] [     ft ] nvim_get_current_buf() is 13
[ 23:57:49:270 ] [ Info  ] [     ft ] nvim_get_current_win() is 1001
[ 23:57:49:270 ] [ Info  ] [     ft ] win count is 2
[ 23:57:49:270 ] [ Info  ] [     ft ] winconfig is {
  anchor = "NW",
  col = 0,
  external = false,
  focusable = false,
  height = 5,
  hide = false,
  mouse = false,
  relative = "editor",
  row = 0,
  width = 168,
  zindex = 50
}
[ 23:57:49:270 ] [ Info  ] [     ft ] real_current_win's buf is9
[ 23:57:49:270 ] [ Info  ] [     ft ] -----------------------------------------------------
[ 23:57:49:270 ] [ Info  ] [     ft ] nvim_get_current_buf() is 9
[ 23:57:49:270 ] [ Info  ] [     ft ] nvim_get_current_win() is 1000


版权声明:本文为原创文章,遵循 署名-非商业性使用-禁止演绎 4.0 国际 (CC BY-NC-ND 4.0)版权协议,转载请附上原文出处链接和本声明。


延生阅读

分享到:

评论