Vim 中缺失的任务/构建系统 - asynctasks.vim

2020-02-11 23:36:11 +08:00
 skywind3000

我看过很多 .vimrc 配置,大部分在关于如何编译和运行项目这方面,都在用一些非常原始的方法,这么多年下来,仍然缺少一套系统化的解决方案。我也用过不少相关插件:neomakequickrun 以及 dispatch,大部分只把事情做到一半,都没能完全满足我的需求。

因此我制作了这个插件,希望能为 Vim 提供一套合理的,系统的构建解决方案。

项目地址: https://github.com/skywind3000/asynctasks.vim

快速入门

本插件在运行时会到当前文件所在目录及所有上级目录搜索所有名为 .tasks 的文件,并先后加载,同样一个名字的任务可以在不同的配置文件里定义多次,目录层次越深的 .tasks 文件拥有越高的优先级。

任务配置文件 .tasks 采用 ini 文件格式,每个 section 定义一个任务,你可以在你某个项目的根目录下面放一个 .tasks 定义一些针对该项目的任务:

# 定义一个新任务
[file-build]
# 定义任务需要执行的命令,以 `$(...)` 形式出现的宏会在执行时被具体替换
command=gcc -O2 "$(VIM_FILEPATH)" -o "$(VIM_FILEDIR)/$(VIM_FILENOEXT)"
# 定义命令运行的目录
cwd=$(VIM_FILEDIR)

[file-run]
command="$(VIM_FILEDIR)/$(VIM_FILENOEXT)"
cwd=$(VIM_FILEDIR)
# 定义输出方式,在终端内运行
output=terminal

上面定义了两个任务,那么当你在 Vim 中编辑该项目的文件时,执行:

:AsyncTask file-build

就可以运行名字为 file-build 的任务了:

默认模式下( output=quickfix ),命令输出会实时显示在下方的 quickfix 窗口中,编译错误会和 errorformat 匹配并显示为高亮,方便你按回车跳转到具体错误,或者用 cnext/cprev 命令快速跳转错误位置。

如果要查看当前有哪些可用任务,则用 :AsyncTaskList 查看有哪些可用任务,然后当你需要编辑任务时,用 :AsyncTaskEdit 打开并编辑当前项目的 .tasks 文件。

有的项目用 cmake 构建,有的项目用 ninjia 构建,基于项目目录的局部配置机制,让你可以定义同样一个任务名称,绑定同样一个快捷键,却在不同的项目里执行特定的构建命令。同时对一些通用性高的项目将任务定义到全局配置里可以避免每个项目写一遍。

是不是很简单?

使用说明

AsyncTask - 运行任务

运行指定任务,格式为:

:AsyncTask {taskname}

这条命令很简单,不过注意命令中各种类似 $(VIM_FILENAME) 的宏,会根据当前文件展开,因此,避免到一个 nerdtree 的工具窗口里去运行任务,会有很多信息缺失导致宏变量展开成空字符串。

AsyncTaskEdit - 编辑任务

编辑任务配置文件:

:AsyncTaskEdit[!]

默认不包含叹号时,编辑的是当前项目的任务配置 .tasks,如果加了叹号,则会编辑全局配置 ~/.vim/tasks.ini

配置文件不存在的话,会预先生产一个配置模板,类似:

# 定义一个名为 "file-compile" 的任务
[compile-file]

# 要执行的命令,文件名之类的最好用双引号括起来,避免包含空格出错。
# 不会写可以用 ":AsyncTaskMacro" 命令随时查看宏变量帮助
command=gcc "$(VIM_FILEPATH)" -o "$(VIM_FILEDIR)/$(VIM_FILENOEXT)"

# 工作目录,可以写具体目录,或者宏变量的名字,$(VIM_FILEDIR) 代表文件目录
# 而 $(VIM_ROOT) 或者直接一个 <root> 则代表项目根目录。
cwd=$(VIM_FILEDIR)

# 任务输出,可以选择 "quickfix" 或者 "terminal"
# - quickfix: 将任务输出显示到 quickfix 窗口并进行错误匹配
# - terminal: 在终端内运行任务
output=quickfix

# quickfix 错误匹配的模板,不提供的话会使用 vim 的 errorformat 代替。
# 为空字符串的话,会让在 quickfix 中显示原始文本
errorformat=%f:%l:%m

# 设置成 1 会在运行前保存当前文件,2 保存所有修改过的文件。
save=1

不同任务配置的优先级是本地配置高于全局配置,深层目录的配置优先于上层目录的配置,概念有点类似 editorconfig,你可以在多级目录定义同样名称的任务,下层的任务会覆盖上层的同名任务。

宏替换

command 字段和 cwd 字段可以使用下面这些宏:

$(VIM_FILEPATH)  - 当前 buffer 的文件名全路径
$(VIM_FILENAME)  - 当前 buffer 的文件名(没有前面的路径)
$(VIM_FILEDIR)   - 当前 buffer 的文件所在路径
$(VIM_FILEEXT)   - 当前 buffer 的扩展名
$(VIM_FILENOEXT) - 当前 buffer 的主文件名(没有前面路径和后面扩展名)
$(VIM_PATHNOEXT) - 带路径的主文件名($VIM_FILEPATH 去掉扩展名)
$(VIM_CWD)       - 当前 Vim 目录(:pwd 命令返回的)
$(VIM_RELDIR)    - 相对于当前路径的文件名
$(VIM_RELNAME)   - 相对于当前路径的文件路径
$(VIM_ROOT)      - 当前 buffer 的项目根目录
$(VIM_CWORD)     - 光标下的单词
$(VIM_CFILE)     - 光标下的文件名
$(VIM_GUI)       - 是否在 GUI 下面运行?
$(VIM_VERSION)   - Vim 版本号
$(VIM_COLUMNS)   - 当前屏幕宽度
$(VIM_LINES)     - 当前屏幕高度
$(VIM_SVRNAME)   - v:servername 的值
$(VIM_INIFILE)   - 当前任务的 ini 文件名
$(VIM_INIHOME)   - 当前任务的 ini 文件的目录(方便调用一些和配置文件位置相关的脚本)

在命令执行前,和上面宏同样名称的环境变量也会被初始化出来。比如你的命令很复杂,你根本用不着把很多宏全部塞在命令行里,可以把任务的 command 设置成调用某 bash 脚本,而在该脚本里直接用 $VIM_FILENAME 这个环境变量就能取出当前的文件名来。

项目目录

按照各类 Vim 插件的通俗约定,asynctasks 以及所依赖的 asyncrun 采用项目标识来定位项目的根目录,从当前文件所在目录一直往上递归到根目录,直到发现某一级父目录中包含下列项目标识:

let g:asyncrun_rootmarks = ['.git', '.svn', '.root', '.project', '.hg']

则认为该目录是当前项目的根目录,如向上搜索到根目录都没找到任何标识,则将当前文件所在目录当作项目根目录。

这些标识文件名你可以配置,如果你有一个项目既不在 git 中,又不在 svn 中怎么办?或者你的 git/svn 的单个仓库下面有很多项目,你并不想让最上层作为项目根目录的话,你只要在你想要的地方新建一个空的 .root 文件就行了。

有了项目位置信息后我们就可以在配置任务时用 $(VIM_ROOT) 或者 <root> 来代替项目位置了:

[make]
command=make
# 设置在当前项目的根目录处运行 make
cwd=$(VIM_ROOT)

[make-run]
command=make run
# <root> 是 $(VIM_ROOT) 的别名,写起来容易些
cwd=<root>
output=terminal

注意,我们定义任务的 .tasks 文件 并不是 项目标识,因为它可以多层嵌套,同一个项目里定义好几个,还会有项目不定义自己的本地任务,只使用 tasks.ini 中定义的全局任务,此时并不需要一个 .tasks 配置放在项目中,因此 .tasks 配置文件和项目标识是两个维度上的事情。

运行模式

配置任务时,output 字段可以设置为:

| 名称 | 说明 | |-|-| | quickfix | 默认值,实时显示输出到 quickfix 窗口,并匹配 errorformat | | terminal | 在终端内运行任务 |

前者一般用于一些编译 /grep 之类的任务,因为可以在 quickfix 窗口中匹配错误。而后者一般用于一些 “纯运行类” 任务,比如运行你刚才编译出来的程序。

当你将 output 设置为 terminal 时,将会根据下面一个全局变量指定终端模式:

" terminal mode: tab/curwin/top/bottom/left/right/quickfix/external
let g:asynctasks_term_pos = 'quickfix'

这个值决定所有 output=terminal 的任务到底用什么终端运行,以及在什么地方打开终端,备选项有:

| 选项 | 模式 | 说明 | |-|-|-| | quickfix | 模拟 | 默认模式,跳过匹配错误,直接在 quickfix 中显示原始输出 | | vim | - | 传统 vim 的 ! 命令运行任务,有些人喜欢这种老模式 | | tab | 内置终端 | 在新的 tab 上打开内置终端 | | top | 内置终端 | 在上方打开一个可复用内置终端 | | bottom | 内置终端 | 在下方打开一个可复用内置终端 | | left | 内置终端 | 在左边打开一个可复用内置终端 | | right | 内置终端 | 在右边打开一个可复用内置终端 | | external | 系统终端 | 打开一个新的操作系统终端窗口运行命令 |

除了指定全局的 g:asynctasks_term_pos 外,在任务配置文件中,也可以用 pos=? 来强制指定该任务需要何种方式运行。

基本上 Vim 中常见的运行模式都包含了,选择一个你喜欢的模式即可。

内置终端

output=terminal 时,设置:

let g:asynctasks_term_pos = 'bottom'

那么运行 :AsyncTask file-run 时,就能在下方的内置终端运行任务了:

终端窗口会复用,如果上一个任务结束了,再次运行时不会新建终端窗口,会先尝试复用老的已结束的终端窗口,找不到才会新建。当使设置为 top/bottom/left/right 时,可以用下面两个配置确定终端窗口大小:

let g:asynctasks_term_rows = 10    " 设置纵向切割时,高度为 10
let g:asynctasks_term_cols = 80    " 设置横向切割时,宽度为 80

有人说分屏的内置终端太小了,没关系,你可以设置成 tab

let g:asynctasks_term_pos = 'tab'

这样基本就能使用整个 vim 全屏大小的区域了:

整个 tab 都用于运行你的任务,应该足够大了吧?在上下左右分屏模式的终端会复用已有窗口而 tab 终端会每次新建新的 tab,要你结束后自己关闭,可以设置:

let g:asynctasks_term_reuse = 1

设置成 1,让 tab 终端也可以先尝试复用已有的 tab。

默认的 quickfix 模式尽管也可以运行程序,但是并不适合一些有交互的任务,比如需要用户输入点什么,quickfix 模式就没办法了,这时你就需要一个真实的终端,真实终端还能正确的显示颜色,这个在 quickfix 是无能为力的事情。

当然,内置终端到 vim 8.1 才稳定下来,处于对老 vim 的支持,asynctasks 默认使用 quickfix 模式来运行任务。

保持焦点

使用分屏模式的内置终端时(left/right/top/bottom),默认运行任务时焦点会跳到终端窗口,这里有个选项:

let g:asynctasks_term_focus = 0

默认是 1,代表改变焦点,如果设置成 0 的话,可以将焦点保持在原来的窗口上,看你个人喜欢。

外部终端

在 Windows 下经常使用 Visual Studio 的同学们一般会喜欢像 VS 一样,打开一个新的 cmd 窗口来运行程序,我们设置:

let g:asynctasks_term_pos = 'external'

那么对于所有 output=terminal 的任务,就能使用外部系统终端了:

是不是有点 VS 的感觉了?目前该选项仅支持 Windows,但是也很容易就能扩展成:

视需求逐步添加完善吧。

系统特定任务

同样一个任务名,你可以配置是 windows 特有,还是 unix 特有:

[task1:win]
command=echo I am in Windows.

[task1:unix]
command=echo I am in Unix.

在任务名后面追加一个冒号,里面可选值为 win 或者 unix,就能指定适配的操作系统了,上面两个任务,尽管都是叫做 task1,但是在不同系统下运行的是不同的命令。

文件类型适配

在同一个任务中,可以在 command 字段后面用冒号分隔写明适配的 &filetype,比如我们写个加强版的 file-run 任务:

[file-run]
command="$(VIM_FILEPATH)"
command:c,cpp="$(VIM_PATHNOEXT)"
command:python=python "$(VIM_FILEPATH)"
command:make=make -f "$(VIM_FILEPATH)"
command:javascript=node "$(VIM_FILEPATH)"
command:sh=sh "$(VIM_FILEPATH)"
command:lua=lua "$(VIM_FILEPATH)"
command:perl=perl "$(VIM_FILEPATH)"
command:ruby=ruby "$(VIM_FILEPATH)"
command:fish=fish "$(VIM_FILEPATH)"
command:php=php "$(VIM_FILEPATH)"
command:erlang=escript "$(VIM_FILEPATH)"
output=terminal
cwd=$(VIM_FILEDIR)
save=2

不加冒号的 command 是默认命令,而加了冒号的 command 如果能和 &filetype 匹配上,则会优先被使用。

把它放到全局配置(~/.vim/tasks.ini )中,绑定 :AsyncTask file-run<F5> 上面,那么每次按 F5 运行同样一个任务时,就能根据当前文件类型自动适配对应命令了,是不是很方便?

配置搜索

前面一直再说全局配置放在 ~/.vim/tasks.ini 中,其实是会搜索所有 runtimepath 的,只要任何一个 runtimepath 中存在 tasks.ini 都会被加载进来,嫌名字丑没关系,你可以定义:

let g:asynctasks_rtp_config = 'etc/tasks.ini'

那么就会变成到每个 runtimepath 下面的 etc 目录中去找 tasks.ini 了。这样设计有个好处,你可以把全局任务配置文件一起提交到你的 dotfiles 仓库里,记得加载的时候设置一下 set rtp+=... 就行了。

对于局部任务配置,就是那个 .tasks 的文件名,不喜欢你也可以换:

let g:asynctasks_config_name = '.asynctasks'

那么就会改为搜索名为 .asynctasks 的文件了。

其他命令

列出当前可用的 task:

:AsyncTaskList

效果如下:

该命令能显示可用的 task 名称,具体命令,以及来自哪个配置文件。

显示宏变量帮助:

:AsyncTaskMacro

显示效果:

左边是宏名称,中间是说明,右边是具体展开值。

这条命令很有用,当你写 task 配置忘记宏名称了,用它随时查看,不用翻文档。

3708 次点击
所在节点    Vim
18 条回复
waruqi
2020-02-12 09:13:13 +08:00
那是因为用 vim 的人大都习惯终端操作了,直接 vim 里面开个终端敲 make 只需要几个字符,省事方便 基本满足需求了,没必要敲这么长的 vim 命令

定位错误信息所在文件,leaderf 过去 +G 定位行,也非常快 所以我都懒得配 quickfix 这种
skywind3000
2020-02-12 10:51:45 +08:00
@waruqi : 谁说要敲那么长的 vim 命令了? map 到 F9 上啊,你敲命令再快,有按一下 F9 快么?你打开内置终端有个切换操作吧?几百行错误信息,你要到终端里去操作滚屏吧?我 bind cnext/cprev 到 f10/f11 就完事了。

quickfix 的好处不光是打开文件和跳转,而是能同众多插件协同,有插件能将 quickfix 中的信息,把文件里错误的那一行标注起来,让你一眼就看到错误在哪里,光标移动过去,下面还有错误提示。有插件能在 quickfix 中用 popup 弹出个小窗口预览,根本不用实际打开文件。。。

vim 一直在进化,现在都 8.2 了,人也应该进化一下,一些 6.4 时代遗留的工作流,是应该拿出来检讨反思一下,比如,为什么其他现代点的编辑器开发程序时,编译运行这件事情,从来不需要开个终端,就只有 vim 需要?那些人都不习惯或者不会用终端么?实际情况就是人家那些编辑器把这个问题解决好了,而 vim 一直没解决,逼着你绕着圈子搞而已,而不是 vim 用户更精通终端。
waruqi
2020-02-12 11:29:40 +08:00
@skywind3000 不要这么激动么,至少对于我这边,不管 map 到哪个 Fx,都要敲 fn + F9 的组合键,没法一键 f9,两个键位离得很远,真心还不如直接敲 make 方便,即使 map 到其它按键,基本上也要组合键,比如 leader+x

而且 vim 里面的终端我是常开的,不需要每次 build 都开一下,这个切换基本可以忽略

几百行错误,追加个 less 就完事了,而且即使滚屏反正有鼠标,滚来滚去看起来也很方便,不觉得有多麻烦,反正又不会切窗口
wizardoz
2020-02-12 12:09:42 +08:00
现在所有 IDE 都安装了 vim 扩展,但是很少直接用 vim 进行编辑。
skywind3000
2020-02-12 14:32:15 +08:00
@wizardoz 这些 IDE 里的 vim 顶多算是一套 keybinding,根本不能算是 vim,连 vim 插件都不支持。我给你举个例子,文本对象,vim-textobj-parameter 提供文本对象,可以让你 "vi," 直接选中一个函数的参数,"ci," 直接改写这个参数,这些真正提升效率的插件,你找个支持的 IDE 来我看看。
kljsandjb
2020-02-12 20:02:49 +08:00
厉害
sampeng
2020-02-12 20:10:05 +08:00
@skywind3000 idea 表示????
怕我是一句的假的 idea…
wizardoz
2020-02-13 10:34:57 +08:00
@skywind3000 vscode 的 neovim 扩展了解一下
wizardoz
2020-02-13 10:37:29 +08:00
@wizardoz 另外,就像楼上说的,IdeaVim 已经相当好了。如果更深层次,比如代码重构、提醒等方面的功能,我相信 IDE 会做的比 VIM 插件更好。
Chingim
2020-02-13 11:26:01 +08:00
其实我觉得,项目的构建 /编译应该是编辑器无关的。

所以把构建和 vim 绑在一起,很不灵活
skywind3000
2020-02-13 12:44:22 +08:00
@wizardoz vscode 的 neovim 扩展不是只是用来运行部分 Ex 指令的么?你用过吗?它支持自定义文本对象么?能跑插件么? IdeaVim 是个模拟器还是子窗口显示 Vim ?
skywind3000
2020-02-13 13:06:08 +08:00
@Chingim 时代在发展,拜托别天天守着 6.4 时代的老旧习惯了。其他编辑器为啥自带的构建 /编译系统用的人非常多,就你 vim 非要依靠个终端或者 tmux ?还不是因为你 vim 6.4 太弱了。现在 8.2 了,时代变了,以前被认为 vim 不适合做的 debug 你去看看 vimspector 现在做的有多专业。
Chingim
2020-02-13 13:20:48 +08:00
@skywind3000 我不是针对谁, 我是说跟编辑器绑定的构建系统都是垃圾

同一个项目应该可以允许使用不同的编辑器来开发 /构建
wizardoz
2020-02-13 14:04:37 +08:00
@skywind3000 neovim 是调用系统的 neovim,你用过吗?
wizardoz
2020-02-13 14:11:25 +08:00
@skywind3000 IdeaVim 是一个独立的扩展,支持文本对象。vscode 的 neovim 插件是调用 neovim 编辑器,neovim 中配置的插件可用。
skywind3000
2020-02-13 15:40:46 +08:00
@Chingim 迫害妄想啊,哪个编辑器拦着你不能在命令行下编译了?就是 vc 都有 msbuild.exe 来做命令行编译。现在大部分项目的构建不都是标准构建工具么? gnumake, cmake, ninjia,msbuild 哪个不能在命令行下用?编辑器所谓集成是帮你在编辑器内调度这些构建工具,并且做好错误分析,标注错误位置。
skywind3000
2020-02-13 16:09:34 +08:00
@wizardoz IdeaVim 支持文本对象是死的么?还是支持扩展?能装 targets.vim 或者 vim-textobj-parameter/indent 之类的插件让我 ci, 改写参数,vii 选中当前缩进么?

vscode 有好几个 vim/neovim 集成,你说的是哪个?

1. 用的最多的 VsCodeVim 插件,也只是个模拟器,里面调用下 nvim 只是执行一些有限的 ex 命令。
2. 还有一个插件就叫 neovim,用的人不多,我试过一下,一堆 bug,翻页时滚动条都在抖来斗去的,我不知道用来干嘛?即使完全没 bug,这样套一个 neovim,它本质上也是 neovim,无外乎是个带了套子的 neovim,这个套子就是 vscode,这种情况下和 neovim 其他 ui 也没什么区别,比如 oni 这些。
kevinhwang
2020-02-14 20:43:25 +08:00
@Chingim 赞同你的观点,其实就是 kiss 原则了。至于 vim 的优点无他,就是让我喜欢上编程。打开速度快,直接就是干。
idea 的 vim 插件说实话确实是残废的,扩展性极差。不过利用 vim 的基本编辑和 idea 内置的功能,已经很好了。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/643892

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX