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

Neovim 窗口 API 参数 noautocmd 测试

2025-11-12
Eric Wong

最近在修改 notify.nvim 插件源码的时候有这么一段:

local win_config = {
  -- .....
}
if not vim.api.nvim_buf_is_valid(buf) then
  buf = vim.api.nvim_create_buf(false, true)
end
if not vim.api.nvim_win_is_valid(win) then
  win_config.noautocmd = true
  win = vim.api.nvim_open_win(buf, false, win_config)
else
  vim.api.nvim_win_set_config(win, win_config)
end

我把 win_config 内容省略了,主要想表达的意思是, 本来我想通过一个固定的窗口配置来打开或者设置窗口的参数。 但是 Neovim 的 nvim_open_winnvim_win_set_config 两个函数所接受的 win_opt 选项是有区别的,已存在的窗口使用后者设置时不能够传入 noautocmd 选项(neovim#36409)。

'noautocmd' cannot be used with existing windows

于是只能分开写,在调用 nvim_open_win 时传入 noautocmd 参数。

那么这个 noautocmd 到底禁用了哪些事件,以及禁用的时机时什么呢?

以下为测试脚本:

local buf = vim.api.nvim_create_buf(true, false)

local log = require('logger').derive('t_no')

local aug = vim.api.nvim_create_augroup('test_noautocmd', { clear = true })

vim.api.nvim_create_autocmd(
  { 'WinEnter', 'BufWinEnter', 'BufEnter', 'WinLeave', 'TextChangedI' },
  {
    pattern = { '*' },
    group = aug,
    callback = function(ev)
      log.info(ev.event)
    end,
  }
)

vim.api.nvim_open_win(buf, true, { split = 'above', noautocmd = true })

-- [ 20:43:20:664 ] [ Info  ] [   t_no ] TextChangedI
-- [ 20:43:23:092 ] [ Info  ] [   t_no ] WinLeave
-- [ 20:43:23:093 ] [ Info  ] [   t_no ] WinEnter
-- [ 20:43:23:094 ] [ Info  ] [   t_no ] BufEnter
local buf = vim.api.nvim_create_buf(true, false)

local log = require('logger').derive('t_no')

local aug = vim.api.nvim_create_augroup('test_noautocmd', { clear = true })

vim.api.nvim_create_autocmd(
  { 'WinEnter', 'BufWinEnter', 'BufEnter', 'WinLeave', 'TextChangedI' },
  {
    pattern = { '*' },
    group = aug,
    callback = function(ev)
      log.info(ev.event)
    end,
  }
)

vim.api.nvim_open_win(buf, true, { split = 'above', noautocmd = false })

-- [ 20:44:50:454 ] [ Info  ] [   t_no ] WinLeave
-- [ 20:44:50:455 ] [ Info  ] [   t_no ] WinEnter
-- [ 20:44:50:456 ] [ Info  ] [   t_no ] BufEnter
-- [ 20:44:50:456 ] [ Info  ] [   t_no ] BufWinEnter
-- [ 20:44:51:279 ] [ Info  ] [   t_no ] TextChangedI
-- [ 20:44:52:045 ] [ Info  ] [   t_no ] WinLeave
-- [ 20:44:52:046 ] [ Info  ] [   t_no ] WinEnter
-- [ 20:44:52:048 ] [ Info  ] [   t_no ] BufEnter

如果去看 API 的源码,neovim 这段 nvim_open_win api 的源码。

Window nvim_open_win(Buffer buffer, Boolean enter, Dict(win_config) *config, Error *err)
  FUNC_API_SINCE(6) FUNC_API_TEXTLOCK_ALLOW_CMDWIN
{
#define HAS_KEY_X(d, key) HAS_KEY(d, win_config, key)
  buf_T *buf = find_buffer_by_handle(buffer, err);
  if (!buf) {
    return 0;
  }
  if ((cmdwin_type != 0 && enter) || buf == cmdwin_buf) {
    api_set_error(err, kErrorTypeException, "%s", e_cmdwin);
    return 0;
  }

  WinConfig fconfig = WIN_CONFIG_INIT;
  if (!parse_win_config(NULL, config, &fconfig, false, err)) {
    return 0;
  }

  bool is_split = HAS_KEY_X(config, split) || HAS_KEY_X(config, vertical);
  Window rv = 0;
  if (fconfig.noautocmd) {
    block_autocmds();
  }

  win_T *wp = NULL;
  tabpage_T *tp = curtab;
  assert(curwin != NULL);
  win_T *parent = config->win == 0 ? curwin : NULL;
  if (config->win > 0) {
    parent = find_window_by_handle(fconfig.window, err);
    if (!parent) {
      // find_window_by_handle has already set the error
      goto cleanup;
    } else if (is_split && parent->w_floating) {
      api_set_error(err, kErrorTypeException, "Cannot split a floating window");
      goto cleanup;
    }
    tp = win_find_tabpage(parent);
  }
  if (is_split) {
    if (!check_split_disallowed_err(parent ? parent : curwin, err)) {
      goto cleanup;  // error already set
    }

    if (HAS_KEY_X(config, vertical) && !HAS_KEY_X(config, split)) {
      if (config->vertical) {
        fconfig.split = p_spr ? kWinSplitRight : kWinSplitLeft;
      } else {
        fconfig.split = p_sb ? kWinSplitBelow : kWinSplitAbove;
      }
    }
    int flags = win_split_flags(fconfig.split, parent == NULL) | WSP_NOENTER;
    int size = (flags & WSP_VERT) ? fconfig.width : fconfig.height;

    TRY_WRAP(err, {
      if (parent == NULL || parent == curwin) {
        wp = win_split_ins(size, flags, NULL, 0, NULL);
      } else {
        switchwin_T switchwin;
        // `parent` is valid in `tp`, so switch_win should not fail.
        const int result = switch_win(&switchwin, parent, tp, true);
        assert(result == OK);
        (void)result;
        wp = win_split_ins(size, flags, NULL, 0, NULL);
        restore_win(&switchwin, true);
      }
    });
    if (wp) {
      wp->w_config = fconfig;
      if (size > 0) {
        // Without room for the requested size, window sizes may have been equalized instead.
        // If the size differs from what was requested, try to set it again now.
        if ((flags & WSP_VERT) && wp->w_width != size) {
          win_setwidth_win(size, wp);
        } else if (!(flags & WSP_VERT) && wp->w_height != size) {
          win_setheight_win(size, wp);
        }
      }
    }
  } else {
    if (!check_split_disallowed_err(curwin, err)) {
      goto cleanup;  // error already set
    }
    wp = win_new_float(NULL, false, fconfig, err);
  }
  if (!wp) {
    if (!ERROR_SET(err)) {
      api_set_error(err, kErrorTypeException, "Failed to create window");
    }
    goto cleanup;
  }

  if (fconfig._cmdline_offset < INT_MAX) {
    cmdline_win = wp;
  }

  // Autocommands may close `wp` or move it to another tabpage, so update and check `tp` after each
  // event. In each case, `wp` should already be valid in `tp`, so switch_win should not fail.
  // Also, autocommands may free the `buf` to switch to, so store a bufref to check.
  bufref_T bufref;
  set_bufref(&bufref, buf);
  if (!fconfig.noautocmd) {
    switchwin_T switchwin;
    const int result = switch_win_noblock(&switchwin, wp, tp, true);
    assert(result == OK);
    (void)result;
    if (apply_autocmds(EVENT_WINNEW, NULL, NULL, false, curbuf)) {
      tp = win_find_tabpage(wp);
    }
    restore_win_noblock(&switchwin, true);
  }
  if (tp && enter) {
    goto_tabpage_win(tp, wp);
    tp = win_find_tabpage(wp);
  }
  if (tp && bufref_valid(&bufref) && buf != wp->w_buffer) {
    // win_set_buf temporarily makes `wp` the curwin to set the buffer.
    // If not entering `wp`, block Enter and Leave events. (cringe)
    const bool au_no_enter_leave = curwin != wp && !fconfig.noautocmd;
    if (au_no_enter_leave) {
      autocmd_no_enter++;
      autocmd_no_leave++;
    }
    win_set_buf(wp, buf, err);
    if (!fconfig.noautocmd) {
      tp = win_find_tabpage(wp);
    }
    if (au_no_enter_leave) {
      autocmd_no_enter--;
      autocmd_no_leave--;
    }
  }
  if (!tp) {
    api_set_error(err, kErrorTypeException, "Window was closed immediately");
    goto cleanup;
  }

  if (fconfig.style == kWinStyleMinimal) {
    win_set_minimal_style(wp);
    didset_window_options(wp, true);
  }
  rv = wp->handle;

cleanup:
  if (fconfig.noautocmd) {
    unblock_autocmds();
  }
  return rv;
#undef HAS_KEY_X
}

从源码中不难看出,这个 noautocmd 选项只是在 nvim_open_win 这个函数调用内起作用,在最后的时候使用 unblock_autocmds 又恢复的事件的响应。


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


延生阅读

分享到:

评论