如何在Neovim中运行命令行任务(附代码)

3,441 阅读6分钟

我的日常工作流程经常涉及重复运行任务,无论是构建命令、单元测试,还是其他一些脚本。我的理想工作流程是在右手边有一个终端分割,然后能够向它发送任务。

内置的终端

Neovim的内置:terminal ,让我可以轻松地在Neovim中打开一个终端作为分割,我有一些自定义的键位来跳转到它。

我没有使用<C-w>{h,j,k,l} 来导航分片,而是重新映射到<C-{h,j,k,l> ,并在终端分片上添加相同的映射,同时映射<Esc>

tnoremap <Esc> <C-\><C-n>
tnoremap <C-h> <C-\><C-n><C-w>h
tnoremap <C-j> <C-\><C-n><C-w>j
tnoremap <C-k> <C-\><C-n><C-w>k
tnoremap <C-l> <C-\><C-n><C-w>l

我也更喜欢在进入终端分片时默认总是处于插入模式:

autocmd BufEnter * if &buftype == 'terminal' | :startinsert | endif

Neoterm插件

为了使事情更容易管理,我还使用Neoterm插件。这使我有能力运行。

:T ls

来打开一个终端,并执行ls 命令。如果你再运行这个命令,它将重新执行终端命令,但在同一个终端。你可能会开始看到这将构成我的终端设置的组成部分,我将在其中持续执行任务......

然后,我可以设置neoterm_sizeneoterm_default_mod 来定义终端的显示方式。我把它设置为屏幕宽度的30%左右,并且在右侧是垂直的:

vim.g.neoterm_size = tostring(0.3 * vim.o.columns)
vim.g.neoterm_default_mod = 'botright vertical'

这个配置是用Lua而不是Vimscript,因为我只使用Neovim。之前的配置代码将被移植,但我还没来得及做。

映射一个键来执行一个命令

我不希望每次想运行测试时都要手动运行:T my-command-here 。所以我开始在需要设置命令的时候手动设置一个快捷方式,比如运行测试:

:nnoremap <leader>e :T npm test<CR>

这很有效,但也有一些注意事项。首先,我使用Neoterm来启动多个终端(我经常在其他标签中为其他命令设置一些终端),而:T 会引用最新的终端。所以为了解决这个问题,我把命令调整为::1T ,这将针对第一个终端。

我还想让终端在每次执行命令之间被清除。我起初是通过使用clear 命令来做到这一点:

:nnoremap <leader>e :1T clear && npm test<CR>

但后来我发现了Neotree的:1Tclear 命令,它可以为我清除终端,而不需要我把clear 作为第一个任务。

我的最后一个问题是,我对手动输入这种映射感到厌烦!所以我开始着手将其自动化。所以我开始着手将其自动化......

建立任务执行命令

我想为<leader>e ,它将会有一个映射:

  1. 如果需要,创建一个终端,或者重新使用现有的终端。
  2. 提示要自动运行的命令。
  3. 一旦给出一个命令,就储存起来,并在以后的运行中再次使用(例如,只提示一次命令)。
  4. 在每次运行之间清除终端。
  5. 提供一个重置命令的选项。

要求(1)和(4)很简单;Neoterm的行为为我免费提供了这些。为了创建一个命令,我可以将其绑定到一个键上,我可以使用 nvim_create_user_command:

vim.api.nvim_create_user_command('TaskPersist', function(input)
  -- implementation here
end, { nargs = '*' })

我已经知道,我可以通过使用Neoterm来执行一个命令(在这里存储为cmd ):

vim.api.nvim_command(":1Tclear")
vim.api.nvim_command(":1T " .. cmd)

所以现在我需要存储一个命令,并找到一种方法来设置它,如果它没有被提供。我可以通过设置一些全局变量来做到这一点,但我希望它是一个漂亮的用户界面这就是nui.nvim出现的地方。我的init.vim 文件中已经有了它,因为我使用的另一个插件依赖于它。而且它有一个Input 的组件!

我创建了一个变量,stored_task_command ,它将是这个Lua模块中的一个变量,用来追踪当前的命令。最初,它被设置为nil

我可以创建trigger_set_command_input 作为一个函数,它将创建并挂载一个Input 组件。当输入被提交时,我们将stored_task_command 设置为输入,并触发一个提供的回调函数。

这个代码的大部分直接来自于nui.nvim 的例子;我只是修改了文本提示和输入的宽度

local stored_task_command = nil

local trigger_set_command_input = function(callback_fn)
  local input_component = Input({
    position = "50%",
    size = {
      width = 50,
    },
    border = {
      style = "single",
      text = {
        top = "Commmand to run:",
        top_align = "center",
      },
    },
    win_options = {
      winhighlight = "Normal:Normal,FloatBorder:Normal",
    },
  }, {
    prompt = "> ",
    default_value = "",
    on_submit = function(value)
      stored_task_command = value
      callback_fn();
    end,
  })

  input_component:mount()
  input_component:on(event.BufLeave, function()
    input_component:unmount()
  end)
end

就这样,我现在可以提供一个漂亮的用户界面来设置命令。

把这些部分组合起来

所以,最终的实现应该是:

  1. 使用Neoterm来创建或重新使用一个现有的终端
  2. 如果stored_task_commandnil ,触发Input 框来设置命令。
  3. 在终端中运行该命令(通过Neoterm),在调用:Tclear
vim.api.nvim_create_user_command('TaskPersist', function(input)
  local execute = function(cmd)
    vim.api.nvim_command(":1Tclear")
    vim.api.nvim_command(":1T " .. cmd)
  end

  if stored_task_command == nil then
    -- Load up the Input component to get a value, then run it
    trigger_set_command_input(function()
      execute(stored_task_command)
    end)
  else
    execute(stored_task_command)
  end
end, { nargs = '*' })

有了这个,就可以了!我可以绑定到一个键:

vim.api.nvim_set_keymap("n", "<leader>e", ":TaskPersist<CR>",{})

而行为也如愿以偿 :)

Executing a task in NeoVim

允许随机的一次性任务

有时候,我确实需要重新使用我的任务运行终端,以快速完成一次性任务。为此,我可以让自定义的TaskPersist 命令选择性地接受一个命令,以用于这一次的运行。

:TaskPersist echo "some one off task"

这可以通过在定义一个自定义命令时读取参数来实现。

vim.api.nvim_create_user_command('TaskPersist', function(input)
  local execute = function(cmd)
    vim.api.nvim_command(":1Tclear")
    vim.api.nvim_command(":1T " .. cmd)
  end

  local one_off_command = input.args

  if one_off_command and string.len(one_off_command) > 0 then
    execute(one_off_command)
  elseif stored_task_command == nil then
    trigger_set_command_input(function()
      execute(stored_task_command)
    end)
  else
    execute(stored_task_command)
  end
end, { nargs = '*' })

nargs 选项设置为* ,告诉Vim这个命令需要任意数量的参数(Vim将命令中的每个空格视为不同的参数)。这些参数会作为input.args ,如果提供了这些参数,我们就可以把它们作为命令执行。

重置命令

我还创建了另一个命令,以使我能够被重新提示存储一个新的命令:

vim.api.nvim_create_user_command('SetTaskCommand', function()
  trigger_set_command_input(function ()
    -- Don't need to do anything here beyond set it
  end)
end, {})

就这样,它就完成了!

我希望你喜欢这个将几个Vim插件和一些Lua结合起来以实现所需工作流程的过程。这篇文章也说明了为什么我如此热衷于钻研Neovim而不是坚持使用Vim。六个月前我从未写过一行Lua,虽然我仍然是一个完全的初学者,但我认为任何有一些编程经验的人都可以掌握这种语言,而且我很喜欢通过写代码对我的编辑经验进行更精细的控制。

如果你想从这篇文章中获取任何代码,你可以在我的dotfiles中找到它,这些文件在GitHub上