使用 Python, Haskell, Ruby 的 REPL 时经常需要能够在一个文本文件中将 当前行或者选中多行文本发送到 REPL 中运行, 并能够灵活地在文本编辑器(执行编辑好的脚本)和 REPL (手工输入并执行一些一次性的代码)间切换。
实现方法总体来说有两种,第一种使用 vim 自己的 terminal, 例如下面的 neovim 自带的 terminal, 第二种是将 editor 和 repl 包裹在 tmux 里。 第一种好处是集成度高,只要 vim 就行了, 缺点是 editor 和 repl 必须在同一台机器上。 第二种优缺点正好相反,需要手工启动 REPL(或者先登录远程主机再启动 REPL), 但比前一种更灵活,适合的场景更广泛。
下面的几种方法使用不同的工具实现了下列目标:
-
:tn
: 创建当前脚本语言对应的 REPL; -
:tl
: 将当前脚本加载到 REPL 中执行; -
:tg
: 显示/隐藏 REPL 窗口; -
使用空格键发送当前行,或者选中行到 REPL 中执行;
-
:tt <your command>
: 在 REPL 中执行<your command>
;
基于 neoterm 的方法的优点是不依赖 vim 以外的工具,适合在不能使用 tmux 的场合 (例如使用 i3wm 时由于快捷键冲突无法使用 tmux)。
Based on neoterm
Add the following lines into $MYVIMRC
:
let g:neoterm_default_mod = 'vertical'
let g:neoterm_autoscroll = 1
let g:neoterm_direct_open_repl = 1
let g:neoterm_repl_ruby = 'pry'
let g:neoterm_repl_python = 'ipython'
autocmd FileType haskell let g:neoterm_repl_command = 'stack ghci'
autocmd FileType haskell,python,ruby nnoremap <silent> <Space> :TREPLSendLine<CR>
autocmd FileType haskell,python,ruby vnoremap <silent> <Space> :TREPLSendLine<CR>
autocmd FileType haskell,python,ruby cabbrev tt T
autocmd FileType haskell,python,ruby cabbrev tn Tnew
autocmd FileType haskell,python,ruby cabbrev tg Ttoggle
autocmd FileType haskell cabbrev tl T :load %
autocmd FileType python cabbrev tl T run %
autocmd FileType ruby cabbrev tl T load '%'
Plug 'kassio/neoterm'
With above configurations, after open a python or haskell file and press
Reload a file with :tl
.
After the job is done, no need to quit with Tclose
, just :qa
.
If you want input something on-the-fly in the REPL, you have 2 options:
-
:tt :set +m
and press Return key.:tt
and pressingbecomes :T
. All characters following is sent to REPL. This is convenient, while you lose the tab-completion functionality in REPL. -
Jump to the REPL window, switch to insert mode with pressing
i
, input something (for examplefilter even [1,2,3]
), leave insert mode with pressingCtrl-\ Ctrl-n
, finally jump back to editor window.
Note: if you want open the terminal window in the bottom instead of right,
use let g:neoterm_default_mod = 'botright'
.
See :h mods
in vim for more options.
Under the hood
If above shortcuts don't work, restart the vim. Or you can run it manually:
-
Open a new terminal window with
:sp | terminal
-
Get terminal window ID with
:echo b:terminal_job_id
-
Run a command, say
ls -la
, with:call jobsend(7, "ls -la\n")
, where the first parameter 7 ofjobsend
is from above step.
Paste mode in GHCi
When send the following lines into repl with neoterm,
there are always errors at the line where ...
:
drink aCup ozDrank = if ozDiff >= 0
then cup ozDiff
else cup 0
where oldOz = getOz aCup
ozDiff = oldOz - ozDrank
The reason is GHCi believe the block is ended after else ...
,
even you've added :set +m
.
The solution is add :{
(with :ts :{
) and :}
before and after the block.
See How to define a function in ghci across multiple lines?.
Based on vim plugin vimux
vimux 能够自动检测tmux pane, 基于它的工作流程是:
为vim安装vimux插件并配置好快捷键(例如下面的配置将快捷键设为空格键)后,
在一个 tmux window 中,左/上侧是vim窗口,右/下侧是repl,例如IPython或者GHCi,
Normal模式下按空格键,将当前行发送到repl中执行,
Visual模式下按空格键,选中的所有文本被整体发送到 IPython 的 %cpaste
区域中
一次性执行。
具体配置方法:
function! Vimux4IPython()
call VimuxRunCommand("%cpaste")
call VimuxRunCommand(@v, 0)
call VimuxRunCommand("^d")
endfunction
nnoremap <Space> :call VimuxRunCommand(getline("."))<CR>
autocmd FileType python vnoremap <Space> "vy :call Vimux4IPython()<CR>
"autocmd FileType python vnoremap <Space> "vy :call VimuxRunCommand("%cpaste")<CR> :call VimuxRunCommand(@v, 0)<CR> :call VimuxSendKeys("^d")<CR>
Plug 'benmills/vimux'
vnoremap
将空格键映射到后面的命令上,这个命令包含两部分:
- 用
"vy
将选中的文本拷贝到 v register 上 - 调用
Vimux4IPython()
向 IPython console 发送命令
VimuxRunCommand 函数的第一个参数是要执行的命令,
第二个参数是执行完 tmux send-keys
后是否再发送一个回车,
所以 VimuxRunCommand(@v, 0)
的第二个参数 0 保证了发送多行文本中间不会插入回车。
VimuxRunCommand()
底层调用了 tmux send-keys
,
所以需要发送 alt
, ctrl
等时参考 tmux send-keys
的写法。
实验发现 VimuxSendText
和 VimuxSendKeys
似乎是异步的,
不能保证前面的命令执行完后再执行后面的命令,导致执行出错,
所以全部使用了 VimuxRunCommand 命令,后来发现这个命令也会出现 "^d" 在 @v
前被发送的情况,
下面被注释掉的目前可以保证顺序执行。
完整内容参考 vem 的 profiles/python.yml。
Note:
另一个执行多行脚本的思路是基于 %autoindent
关闭 IPython console 的代码自动缩进,
然后完全模拟多行输入,这个方案的问题是 %autoindent
只能在 on/off 间切换,
不能用 %autoindent 0
之类的方法保持关闭状态,导致多次执行代码块时无法确定
autoindent 是否关闭,可以用 ipython --no-autoindent
等方法保持其始终关闭的状态,
在 IPython 里手工输入时,再手工开启,麻烦且容易出错。
指定 REPL 运行窗口
先执行 :let g:VimuxRunnerIndex='3.1'
,再空格。
vimux 代码分析
vimux 全部代码位于 plugin/vimux.vim 中,代码量比较小。
执行命令 (VimuxRunCommand
) 流程:
-
若未定义
g:VimuxRunnerIndex
, 或者g:VimuxRunnerIndex
值对应的 tmux 窗口不存在, 则通过执行VimuxOpenRunner
确定g:VimuxRunnerIndex
的值(详细过程见下面); -
若函数调用包含了第二个参数 (
exists("a:1")
) 则在发送完命令后在发送回车 (VimuxSendKeys("Enter")
) -
定义复位指令,将当前命令记录在
g:VimuxLastCommand
中, 供后续执行VimuxRunLastCommand
命令调用; -
发送 tmux 复位指令(取消未发送完的命令):
call VimuxSendKeys(resetSequence)
-
发送用户指定的命令文本:
call VimuxSendText(a:command)
确定 REPL runner (VimuxOpenRunner
) 流程:
-
获取最近的 REPL 窗口(下面详述);
-
若要求使用最近窗口(
_VimuxOption("g:VimuxUseNearest", 1) == 1
) 且此窗口存在(nearestIndex != -1
), 则将 REPL runner 定义为此窗口(let g:VimuxRunnerIndex = nearestIndex
); -
否则根据
g:VimuxRunnerType
的值新建一个 pane (tmux split-pane -p ...
) 或者 window (tmux new-window
) 作为 REPL runner
获取最近的 REPL 窗口(_VimuxNearestIndex
):
-
列出当前 window 中的所与 panes:
_VimuxTmux("list-"._VimuxRunnerType()."s")
, 实际上执行了tmux list-panes
,因为g:VimuxRunnerType
默认值为 'pane'; -
若所有 pane 均为 active(实际上是只有一个 active pane,
for
循环只运行一次), 则返回 -1,否则返回第一个不为 active 的 pane 的标识符 x.y;
Note:
根据上面确定 REPL runner 的规则,不指定 g:VimuxRunnerIndex
的情况下,
REPL pane 可以在 vim pane 的左侧或者上面。
vimscript 中调用函数有两种方法:直接执行函数时要加 call
,
用于其他函数的参数(最常见场景是打印函数返回值 :echo myfunc()
)
或者表达式时不加 call
。
Vimscript functions 查询 vimscript 函数很方便。
Based on Tmux send-keys
与 vimux 的原理相同,也是在 tmux 中利用 send-keys
命令将当前或者选中的文本
发送到目标 pane 中。
目录结构:当前工作目录下有 sendcmd.vim 和 test.md 两个文件, 前者包含实现功能的 vim 函数,后者是包含要执行命令行的数据文件。
开发环境:tmux window 1 做sendcmd.vim开发,window 2 上下拆分为两个pane ,上面 (ptop) 是命令行,下面的 pane (pbottom) 中用 vim 打开数据文件test.md.
每次修改并保存sendcmd.vim后,在test.md中执行:so sendcmd.vim
,
然后把光标移动到要执行的命令所在的行上按F3
键就可以看到命令被发送到ptop里的
执行效果了。
格式探测
这一步要解决的问题是:怎样的字符串,才能被tmux send-keys
正确地传送出去,
并能正确的执行。
首先要保证运行tmux send-keys
不报错,如果报错,
将第5行的输出拷贝到一个单独的命令行中运行,解决错误。
这一步通过后,如果命令被传送到ptop
后报错,根据错误日志解决之。
sendcmd.vim:
function! SendCmd()
let curline = getline(".")
echom curline
let cmd = "tmux send-keys -t top '" . curline . "' Enter"
echom cmd
echom system(cmd)
endfunction
nnoremap <F3> :call SendCmd()<CR>
说明:
- 为什么第4行中
curline
要用单引号(而不是双引号)包裹?
因为要执行的命令中可能包含变量(如es, idx等),双引号包裹的变量会被求值,
而我们的要求是这些变量不能被求值,要等到被发送到ptop后再被求值。由于这个原因,
第4行包裹外层tmux send-keys
的命令就只能用双引号包裹。
- vimscript如何连接字符串?
同样参考第4行,用点( . )连接。
tmux send-keys -t top
可以将keys从下面的pane发给上面, 也可以从左侧发到右侧,但如果是在右侧pane里执行这个语句,则会被发给自己。
test.md:
es=http://192.168.100.231:9200
api=http://192.168.100.231:8000
idx=production
type=Fair
# get elasticsearch version
http $es | jq '.version.number'
# list all indices
http -b GET $es'/_cat/indices?v'
http -b GET $es'"'"'/_cat/indices?v'"'"'
# list all types of a index
http -b GET $es/$idx/_mapping|jq ".$idx.mappings|keys"
# list all properties of a type
http -b GET $es/$idx/_mapping|jq ".$idx.mappings.$type.properties|keys"
# get objects count in a type
http -b GET $es/$idx/$type/_count|jq '.count'
http -b GET $es/$idx/$type/_count|jq '"'"'.count'"'"'
# query result count
http -b POST $es/$idx/$type/_search query:='{"bool":{"must":[{"query_string":{"query":"五金机械"}}]}}' | jq '.hits.total'
http -b POST $es/$idx/$type/_search query:=''{"bool":{"must":[{"query_string":{"query":"五金机械"}}]}}''
http -b POST $es/$idx/$type/_search query:='"'"'{"bool":{"must":[{"query_string":{"query":"五金机械"}}]}}'"'"'
http -b POST $es/$idx/$type/_search query:='"'"'{"bool":{"must":[{"query_string":{"query":"五金机械"}}]}}'"'"' | jq '"'"'.hits.total'"'"'
通过不断尝试后,发现用'"'"'
代替'
就能正确的发送并执行,
见第10,11行,20,21行,24~27行。
下面以第10行为例分析其结构:
-
第一个单引号与外层的单引号结合,包裹它们中间的内容
http -b GET $es
, 保证里面的特殊字符($
)不会被求值; -
中间的
"'"
是一组,实现被tmux
发送后,最终执行的命令行里仍有一个单引号 ($es
后面的单引号); -
最后的单引号,与后面的
'"'"'
中最左边的单引号组合, 包裹中间的内容/_cat/indices?v
,如果没有这一组单引号, 里面的问好就会被求值,导致命令无法执行。
这一步实现了在第11、21、27行上按F3
键可以正确执行。
整合进vim function
这一步将前面的测试结果放进vim函数中,实现在第10、20、24行上能够正确执行。
sendcmd.vim:
function! SendCurLineInTmux()
let curline = getline(".")
let escstr = substitute(curline, "'", "'\"'\"'", 'g')
let cmd = "tmux send-keys -t right C-u '" . escstr . "' Enter"
" echom cmd
echom system(cmd)
endfunction
nnoremap <silent> <Space> :call SendCurLineInTmux()<CR>
vnoremap <silent> <Space> :call SendCurLineInTmux()<CR>
这里要注意的是 tmux send-keys -t right C-u
中的 C-u
,是为了解决 IPython
的自动缩进问题,在 GHCi 中不存在这个问题,但加一个 Ctrl-u 也无妨。
IPython低版本中可以用 %autoindent
关闭自动缩进,但在6.2版本中这个方法不起作用了。
另外可以用 %cpaste
达到无缩进粘贴的目的,但要用 Ctrl-D
或者 --
表示输入结束,
都不如在每一行首加 Ctrl-u
方便。
将上面的 sendcmd.vim 中的内容加入到 $MYVIMRC 中就可以了。
制作成插件
当功能基本定型后,可以把脚本变成插件,避免手工修改配置文件。 但对于很小的脚本来说,直接加入配置文件中也不麻烦。
最简单的vim插件,只要在一个目录(tmuxcmd)里创建一个plugin
目录,
把.vim脚本放进去,然后把目录做成git库就行了:
mkdir -p tmuxcmd/plugin
mv test.vim tmuxcmd/plugin/tmuxcmd.vim
cd tmuxcmd
git init
git add ...; git commit -m "..."
在.vimrc里加入这个插件:增加一行代码:Plugin 'file:///home/leo/temp/tmuxcmd'
安装:vim +PluginInstall
安装过程实际是把代码库clone到~/.vim/bundle下, 修改~/.vim/bundle/tmuxcmd/plugin/tmuxcmd.vim文件在新的vim编辑器中不会生效。
本地插件需要保存一个本地目录,更简单的方法是发布到github上,再用vundle安装:
-
把代码库push到github上: leetschau/tmuxcmd
-
在.vimrc中,把原来的
Plugin 'file:///home/leo/temp/tmuxcmd'
换成Plugin 'leetschau/tmuxcmd'