在 Vim 里控制外部程序,Vim 与 Emacs 与 Conque - Jacky Liu's Blog
在 Vim 里控制外部程序,Vim 与 Emacs 与 Conque
---- 上一篇里说的想拿 Vim 当自编程序的 UI,基本已经做到了:
---- 底部的那个 buffer 属于 CSA 模块,这个新写的 Vim 插件就是负责充当外部程序在 Vim 里的界面,buffer 里显示的是外部程序的输出。关于用 Vim 来当 UI 的话题在 第一篇 里讲过。
---- 果然即使用 Python 接口建立了多个线程,也是没有办法让这些线程自动刷新 Vim buffer 里的显示的,况且还有上一篇里说的那个大 Bug。最后采用比较妥协的办法,负责监听的辅助线程还是建的,但是不产生任何输出,只修改后台数据。而主线程每次向外部程序发送输入之后,就 sleep 0.3 秒然后根据后台数据刷新显示,一般情况下都能即时显示外部程序的输出。另外再定义一个手动刷新的按键。
---- 要点记一下:
1. 上一篇里说的那个 bug,是 GUI 的问题,用终端版的 Vim 没事。GUI 版如果在后台建多个线程的话,只允许其中一个线程有可见的输出(也就是造成 Gvim 窗口内显示内容改变),其它线程只能修改后台数据。
2. 如上,即使多线程也不可能自动刷新 Vim buffer 的内容,但可以显示消息(不考虑 bug 的情况下)。自动刷新 Vim buffer 目前已知的只有一个办法:用 VimScript 的 feedkeys() 函数,但是有严重局限,相当于屏蔽了用户的其它输入。这个绝对不用。
3. 辅助线程监听外部程序的输出用的是 Python 的 select.select() 函数,可以监听一个 file descriptor 列表并返回其中有内容可读的 descriptor,但有一点要注意:在产生输出的外部程序已经终止以后,会出现 file descriptor 一直有效但读不到内容的情况,如果不注意这一点就有可能让监听过程陷入死循环,知道的话只要稍微处理一下就可以了。
---- 下一步打算升级程序的 Plotting 模块。现在这个模块还是测试性质的,上次试着画了 2000 多只股票自 2010 年以来的走势,全部单线程运行,花了两个多小时。现在打算改用 Python 的 multiprocessing 模块实现。据文档里说,这个模块与 threading 模块的接口十分类似,这就好了,以前那些关于 threading 的代码可以直接拿过来抄。以后批量执行绘图任务的时候,应该能省不少时间。
----------------------------------------------------- <以下补记> -------------------------------------------------------
---- 重大发现,感谢 依云 在回复里的提醒。以上要点第 2 条其实是不成立的。Vim 本身虽然是单线程的,但是通过 Python 完全能够实现多线程的特性。上面所说的辅助线程在监听到外部程序输出以后,就可以操作 vim.buffer 对象,这时 Vim buffer 实际上已经更新了,所缺的只是刷新显示的步骤而已。只要在刷新过程的最后添上一条 Vim 命令: "redraw!" 就可以了,出来的效果实在是 nice!
---- 这样一来,我完全想不到有什么是 Vim + Python 不能做的了。想起来在去年的时候,我想拿 Vim 当自编程序的界面,因为在 Vim 里找不到与外部进程交流的办法而被逼到去看 Emacs 。。。 如今看来,Vim + Python 真是个无敌组合,我忍不住要恶心一把。。。Emacs 你去 Shǐ 吧,哈哈,你的 Elisp 再强,能强得过 Python 吗?速度比不过 Vim,功能又比不过 Python。再想想 Elisp 里头那无数个括号,是要蛋疼到怎样啊?你那 N 个窗口,图片显示功能或许很酷,可是搁不住它慢啊!这事情一慢起来还有什么意思?好在 Emacs 早就顾及这一点,于是定义了更加蛋疼的操作方式,用 Emacs 的人,想快也快不起来,啊哈哈哈 ...
---- 以上讲笑,请浮云看待。如有用 Emacs 的看到,请先冷静 10 秒开骂 。。。
---- 既然这样,Conque 这个 Vim 插件就好拿出来再说一下了。确实像 依云 说的那样,它的实现可以有重大改进。Conque 是在 Vim 里模拟 Shell 终端的一个很流行的插件,它不同版本的实现曾经有过很大改变,但都使用了 Python 接口,插件负责底层交流的部分是 Python 写的。我在去年 7 月左右看过当时最新版的代码,学习了很多。首先,知道使用 Python 接口来编写 Vim 插件就是来自 Conque 的启发,这个还要感谢当时在 Vim-cn 群里跟我提起这个插件的网友 “Strange”。另外,Conque 使用了 Python 语言里负责底层系统操作的标准模块,比如 tty、select 这些,来与外部 Shell 进程交流,这些模块直接对应于底层的 Unix 系统调用,效率高而且可靠,我在自己写的程序里也用 select.select() 来监听外部进程,这也是从 Conque 里学来的。
---- 从当时的 Conque 的代码来看,它是单线程的,从键盘接收用户输入发送给 Shell 进程,而接收靠的是 Vim 函数 feedkeys(),以这个函数作为触发,不停地监听和提取 Shell 输出,经过格式、颜色处理以后发送到 Vim buffer 里。就像上面说的,用 feedkeys() 有严重局限,因为它完全模拟了用户的按键操作。这样一来,真正的用户就只能进行一个键的操作了。如果按键输入多于一个键,后面的键就会被 feedkeys() 冲掉。而 Conque 的解决办法,就是把键盘上几乎每一个键都做了映射,使得 Conque 能通过这些映射来区分是真正的用户输入,还是 feedkeys() 的输入,从而做不同处理。我觉得,这是个很无奈的做法。如果它对速度的影响还不那么糟糕的话,实际上它还屏蔽了 Vim 强大的编辑功能。如果只是把 Shell 原样不动地搬进 Vim 的窗口里是没多大意思的,能够在 Vim 里操作 Shell 的最大好处在于,在编辑那些火星文的 Shell 语句的时候,要能够毫无保留地使用 Vim 的强大编辑功能,这样才真正有意义。
---- 今天这个突破应该能够让以上成为现实,或许一半成为现实。。。因为还有 GUI 的那个大 Bug 在。是不是 Conque 的作者没有用多线程是因为,他当时就知道这个 bug 啊?为了顾及到插件的通用性,就只好放弃这个几近完美的解决方案了。因为这是 X 系统的 bug,我就叫你 X-bug 吧,万事具备,只欠踩死这只 bug!哈哈!
2011年4月28日 12:30
你试试 :redraw 命令刷新?
2011年4月28日 12:45
试过了,可行!不过要加 !,不然状态栏不会更新。用此方法实现 conqueTerm 效率应该会很高。
2011年4月28日 15:41
@依云: 哈哈,太好了!这个重大突破,这篇我要更新一下 。。。
2011年4月28日 17:26
@依云: 我原先以为,只要不是最老早的那种命令行界面,或者说只要是带个窗口的东西,包括 Terminal,都用的是 X server,那个 bug 有可能不是 X 的问题,不是这样么?
2011年4月28日 17:52
@蓝色基因: Terminal 用了 X,但是终端版的 Vim 没有。
另外,如果不准备在 Win 上用的话,用 poll() 比 select() 效率更高。
另外,无论是 bash 还是 zsh,都是可以在编辑命令行时调用编辑器来编辑的(只可惜这样就不容易查历史了)。
最后,我想尝试下 Lua 的 coroutine 了。
2011年4月28日 18:02
@依云: Lua 的 coroutine 好像不行,把 Vim 折腾得没反应了。。。
2011年4月28日 18:11
@依云: Hmmm... Interesting!
2011年4月29日 07:53
@依云: 为什么用 Lua?如果那是 X 的问题,改用 Lua 不是结果也一样么?
2011年4月29日 11:30
@蓝色基因: 因为 Lua 的那个叫协程,和线程不同。不过好像更行不通。。。
2011年7月04日 14:14
别折腾了, 改vim源码是最好的方法. vim现有的设施限制是非常大的. 需要改源码加新特性. 我最需要的是冻结列特性!
2011年7月05日 03:05
@fanhe: why,那个 bug 看起来是 X 的问题不是 vim 的问题。
2013年12月16日 13:41
Vim使用python进行多线程的时候,后台线程不能对Vim的GUI进行操作,不能执行Vim命令,但似乎可以使用Vim的client-server功能对Vim远程发送命令,达到执行前台Vim命令的目的。
2013年12月18日 00:58
@Kanato: 对,可以后台另开一个 Vim 进程,绕过 GUI 的那个 'bug'。在 vim client 里:
定时激励进程= subprocess.Popen(['vim', '-u', 'NONE', '--servername', 'vim-server', '-S', '定时激励.vim', '-e', '-s'], shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
"定时激励.vim" 里用一个无限循环通过 remote_send() 或 remote_expr() 不断地给 vim client 提供激励,使后者调用预设的入口函数,可以在 vim client 上实现异步多线程的效果。不用的时候可以 kill 掉:
定时激励进程.kill()
需要的时候可以再打开。这里的关键在于通过 subprocess 模块开启 vim 进程(不是 gvim)来后台运行,免得另开一个窗口。
2013年12月18日 13:48
请问如果要让python与这个“定时激励进程”进行通讯,你是如何做到的呢?谢谢
2013年12月18日 14:00
就比如,“定时激励进程”的工作是不断接受python的命令,一旦接收到就转发给前台vim,这样达到python间接控制前台vim的目的,不知能否做到呢?
2013年12月18日 14:29
哈哈,刚刚想了个方法:
while 1
let keys = input(':')
call remote_send('GVIM', keys)
endwhile
这样,python可以通过管道给这个进程发送指令,间接地被转发给前台的gvim,应该就可以解决了
2013年12月18日 15:43
@Kanato: 激励进程(server)唯一的任务就是每隔一小短时间给 client 发送一个外部激励,让 client 实现多线程的效果。client 不需要向 server 发送信息,不用时直接 kill 掉就行了。如果 client 退出运行,作为子进程的 server 也会跟着退出。
2013年12月18日 15:53
我懂你的意思了。不过我跟你想的方向不太一样。我想的是使用一个跟 vim 无关的 python 进程(或者在前台的 vim 中用 python 开一个线程也行),这个进程/线程再开一个 vim,并且跟 python 进程/线程使用管道连接,而这个后台 vim 进程运行我上面的 vim 脚本,这样就可以做到 python 通过管道给这个后台 vim 发送按键,而后台 vim 只要一遇到 '\n' 时就会自动转发给前台 vim。
2013年12月18日 17:08
@Kanato: 你们都在这里 hack 不去 vim_dev 上试试那两个补丁么?
2013年12月18日 18:12
@依云: 能告诉一下是哪两个补丁么?
2013年12月18日 19:31
招认了吧,俺就是个比较低端的用户。跟大虾们开拓创新相比,俺能做的只是在圈好的地里吃草而已。vim_dev 是大虾们去的地方,俺连翻墙也不会,更不要说去翻那些浩如烟海的补丁了。依云 这个大天使又降临了,你头上那个圈儿俺看着像宝箱里的宝贝。到底啥补丁???
2013年12月18日 19:40
@依云: 还有,这个问题能不能转告大虾们一声,不要一劈腿没有掩饰好就搞绝杀,有没保存的文件咋办呢?太粗暴了,就算换个文雅点的拒绝方式也行。
2013年12月19日 18:39
@Jacky Liu:
https://groups.google.com/d/msg/vim_dev/-4pqDJfHCsM/LkYNCpZjQ70J [PATCH] Asynchronous functions (settimeout, setinterval, and cancelinterval)
https://groups.google.com/d/msg/vim_dev/65jjGqS1_VQ/fFiFrrIBwNAJ [PATCH] Proof of concept: thread-safe message queue
没保存的文件会留下 swap 文件的。这个问题是 X Window 的特性导致的。
2013年12月22日 00:12
@依云: 第一个补丁不是多线程,好像是一个延时执行函数。我还想不出来它能干什么。
第二个好像是,但是从它目前的状态来看显然不是我等能去尝鲜的。
我觉得有意思的是 Bram Moolenaar 回复了第一个帖子,却没回复第二个,他显然也是倾向于通过 Python 来获得多线程特性的(跟我们一样),但是却对任何想在 Vim 里多加一个线程的做法不感兴趣。所以我想 GUI 的那个问题基本上就这样了。
我觉得我的那个 hack 还是目前最可行的办法,另开一个 Vim 进程来充当线程的角色。完全后台运行,没有多余的窗口,主进程关闭的话也跟着关闭。我试了不少次才达到这效果。
2013年12月22日 00:17
@依云: 而且,第二个补丁就算弄成了,是否能解决 GUI 的问题可能还是未知数吧。
2013年12月22日 11:48
@依云: 个人觉得,要让 Vim 可以更好地配合 Python 多线程工作,并不一定需要在 Vim 中新加线程。比如,最简单粗暴的做法,在 vim.eval()、vim.command() 之类的函数里加上全局锁,并且在 Python 接口部分提供方法来获得这个锁的引用。个人水平有限,不知这样是否可行。
2013年12月22日 14:40
@Kanato: 不行的。必须从主线程访问 GUI。
所以 settimeout 这种就有用了啊,Python 线程调用 settimeout 让 Vim 主线程去调用指定的函数,于是访问 GUI 还是在主线程,于是就不会崩溃了。
2013年12月22日 15:03
@依云: 嗯。但我觉得 settimeout 这个还不是根本解决问题。最好能有一个机制允许 Python 的后台进程和 Vim 主线程之间进行同步。
2023年5月19日 19:38
पुरानेमॉडलपत्र is a initiative of professional writers who have come together for dedicated news coverage of latest happenings around the country (India). Our पुरानेमॉडलपत्र.com team comprises of professional writers & citizen journalists with diverse range of interest in Journalism who are passionate about publishing the Education Updates with transparency in general public interest.