原文地址:http://www.ibm.com/developerworks/cn/linux/l-cn-emacs-shell3/index.html
在《大话 Emacs Shell Mode 》的第一部分里面介绍了 GNU Emacs 多窗口工作模式的一些特点。简单、便捷而又富于变化的多窗口工作模式使得在 GNU Emacs 当中进行上下文参照和进行多任务处理成为很方便的事情。比如按下组合键 Ctrl-x 2
或者 Ctrl-x 3
就是最常见的两窗口模式,多按几下这些组合键就是经典的四窗口模式。或者,还可以更简单一些,编写一个函数,把四窗口模式的创建绑定到一个组合键上去。
清单 1. 四窗口模式的创建绑定到一个组合键上去
; +----------+-----------+ ; | | | ; | | | ; +----------+-----------+ ; | | | ; | | | ; +----------+-----------+ (defun split-window-4() "Splite window into 4 sub-window" (interactive) (if (= 1 (length (window-list))) (progn (split-window-vertically) (split-window-horizontally) (other-window 2) (split-window-horizontally) ) ) ) (global-set-key (kbd "C-x 4 4") |
这样是不是比起在 .Xdefaults
文件当中精心计算每一个窗口的 geometry 要舒服多了?虽然 .Xdefaults
文件的设置看似可以“一劳永逸”,但是想一想某天心血来潮要调整一下 X terminal 里的字体大小的时候会发生什么? 还有一点不要忘记,在 GNU Emacs 里面窗口模式与里面的内容是相互“分离”的,他们并不是绑在一起的。所以可以随时将 同一个Shell buffer 里面的内容展示在两个不同的窗口里面进行上下文的参照,这个优势是 X terminal 的窗口方式完全无法提供的了。
当然,事情并非表面上看起来那么完美。当我们仔细审视就会发现,现有的多窗口工作方式依然存在一些不完善的地方。两窗口的模式显然很明显了,窗口太少,多任务处理的时候明显不够使。四窗口 ( 或者更多的六窗口、九窗口,如果你有更大的显示器的话 ) 模式虽然在实践当中是一种比较常见的使用方式,但是呢,窗口越多,尺寸越小,很多时候都会遇到面积不够用的情况,比如说打开一个较大的日志文件,或者就是简单的执行了一条长模式的 Shell 命令 ( ls -l
或者 ps -ef
…… ),这个时候狭小的显示面积就会成为每一个小窗口无法释怀的痛。人们通常的做法常常是临时放大当前这个窗口,阅读完毕之后再恢复回去。
清单 2. 临时放大当前这个窗口
+----------+-----------+ +----------------------+ | | | / \ | | | | | /+-------+\ | | +----------+-----------+ \+-------+/ | | | | | \ / | | | | | | | +----------+-----------+ +----------------------+ |
在 GNU Emacs 里面可以使用 C-x 1
组合键放大窗口,然后使用 C-c <left>
组合键 [1]。( 如果你使用的是 X terminal,在 Gnome 桌面环境下可以使用 Alt-F10
组合键放大窗口, Alt-F5
组合键还原窗口 )
但是,一旦这种切换变得非常频繁的时候 ( 想想 ls -l
是一个多么常用的命令吧 ),就会成为一个恼人的负担。优秀的工具应该用起来是舒心的。切换即使不能完全避免,至少也应该尽可能的减少。如果一个窗口需要频繁运行长模式的命令,为什么不干脆给它一个大尺寸呢? 说的没错。既要有大尺寸,又要有多窗口,真正平衡的选择实际上应该是三窗口模式。
三窗口模式是大尺寸和多窗口的一个很好的平衡。总有一个大窗口,要么长度是全尺寸的,要么高度是全尺寸的,将需要较大显示面积的任务放到这里运行,尽量减少切换的次数,让我们能够更加关注于自己的工作。
当然了,针无两头尖,新方案的引入也同时带来了新问题。三窗口模式有两种不同的表现方式,究竟选择哪一种就成了一个问题。
清单 3. 三窗口模式有两种不同的表现方式
+----------+-----------+ +-----------+-----------+ | | | | | | | | | | | | +----------+-----------+ | +-----------+ | | | | | | | | | | +----------------------+ +-----------+-----------+ |
很显然,横向排布的大窗口更适合运行长模式命令的工作状况,而竖向排列的大窗口更适合检查日志文件时的工作状况。这样的工作状况在现实中是不断的交替出现的,根本就没有一个非黑即白的状况。如果可以两者兼得,而不是必须作出选择,那是多么美妙的事情啊!
这就是 GNU Emacs 充分发挥灵活性的优势的地方了。只需要编写两个简单的函数就让 GNU Emacs 在这两种表现方式之间自由切换,就不必非要做出选择了。
下面这个函数是把横向布局的三窗口模式转变成竖向布局和三窗口模式。代码的注释部分的示意图形象地说明了函数的功能。
清单 4. 函数的功能
; +----------------------+ +------------+-----------+ ; | | \ | | | ; | | +-------+\ | | | ; +----------+-----------+ +-------+/ | +-----------+ ; | | | / | | | ; | | | | | | ; +----------+-----------+ +------------+-----------+ (defun split-v-3 () "Change 3 window style from horizontal to vertical" (interactive) (select-window (get-largest-window)) (if (= 3 (length (window-list))) (let ((winList (window-list))) (let ((1stBuf (window-buffer (car winList))) (2ndBuf (window-buffer (car (cdr winList)))) (3rdBuf (window-buffer (car (cdr (cdr winList)))))) (message "%s %s %s" 1stBuf 2ndBuf 3rdBuf) (delete-other-windows) (split-window-horizontally) (set-window-buffer nil 1stBuf) (other-window 1) (set-window-buffer nil 2ndBuf) (split-window-vertically) (set-window-buffer (next-window) 3rdBuf) (select-window (get-largest-window)) )))) |
这个函数是把竖向布局的三窗口模式转换成横向布局。
清单 5. 转换成横向布局
; +------------+-----------+ +----------------------+ ; | | | \ | | ; | | | +-------+\ | | ; | +-----------+ +-------+/ +----------+-----------+ ; | | | / | | | ; | | | | | | ; +------------+-----------+ +----------+-----------+ (defun split-h-3 () "Change 3 window style from vertical to horizontal" (interactive) (select-window (get-largest-window)) (if (= 3 (length (window-list))) (let ((winList (window-list))) (let ((1stBuf (window-buffer (car winList))) (2ndBuf (window-buffer (car (cdr winList)))) (3rdBuf (window-buffer (car (cdr (cdr winList)))))) (message "%s %s %s" 1stBuf 2ndBuf 3rdBuf) (delete-other-windows) (split-window-vertically) (set-window-buffer nil 1stBuf) (other-window 1) (set-window-buffer nil 2ndBuf) (split-window-horizontally) (set-window-buffer (next-window) 3rdBuf) (select-window (get-largest-window)) )))) |
这两个函数很简单。他们实际上只做了三件事:
1. 首先移动到最大窗口
2. 判断当前是否是三窗口布局,如果是,保存当前活动缓冲区的名称
3. 生成新的窗口布局,同时将保存的活动缓冲区放回对应的窗口中。
就是这么简单,其实只是将手工操作的过程交由 GNU Emacs 去做了而已。如果我们把这两个函数绑定到两个组合键上就可以交工了。
只是,这样做显然有点不够精彩。尤其是还需要用户去操心应该在什么状况下调用哪一个函数,这是一件很讨厌的事情。应该把这两个函数结合起来,然后让 GNU Emacs 去操心在什么时候应该做什么,这样才舒服嘛。
上节说的就是下面这个函数。在这个函数当中 GNU Emacs 去操心当前究竟是哪种模式 ( 比较一下窗口的宽高就知道了嘛 ),以及应该旋转成哪种模式 ( 当然是“另一种”模式啦,一共就两种嘛 )。
清单 6. 让 Emacs 自己选择旋转函数
; +------------+-----------+ +------------+-----------+ ; | | | \ | | | ; | | | +-------+\ | | | ; +------------+-----------+ +-------+/ +------------+ | ; | | / | | | ; | | | | | ; +------------+-----------+ +------------+-----------+ ; +------------+-----------+ +------------+-----------+ ; | | | \ | | | ; | | | +-------+\ | | | ; | +-----------+ +-------+/ +------------+-----------+ ; | | | / | | ; | | | | | ; +------------+-----------+ +------------+-----------+ (defun change-split-type-3 () "Change 3 window style from horizontal to vertical and vice-versa" (interactive) (select-window (get-largest-window)) (if (= 3 (length (window-list))) (let ((winList (window-list))) (let ((1stBuf (window-buffer (car winList))) (2ndBuf (window-buffer (car (cdr winList)))) (3rdBuf (window-buffer (car (cdr (cdr winList))))) (split-3 (lambda(1stBuf 2ndBuf 3rdBuf split-1 split-2) "change 3 window from horizontal to vertical and vice-versa" (message "%s %s %s" 1stBuf 2ndBuf 3rdBuf) (delete-other-windows) (funcall split-1) (set-window-buffer nil 2ndBuf) (funcall split-2) (set-window-buffer (next-window) 3rdBuf) (other-window 2) (set-window-buffer nil 1stBuf))) (split-type-1 nil) (split-type-2 nil) ) (if (= (window-width) (frame-width)) (setq split-type-1 'split-window-horizontally split-type-2 'split-window-vertically) (setq split-type-1 'split-window-vertically split-type-2 'split-window-horizontally)) (funcall split-3 1stBuf 2ndBuf 3rdBuf split-type-1 split-type-2) )))) |
现在我只需要把这个函数绑定在 C-x 4 c
组合键上,就可以方便、快捷的在三窗口模式的两种布局之间进行方便的切换。
(global-set-key (kbd "C-x 4 c") (quote change-split-type-3)) |
现在窗口可以自由切换了,那么假如我想保持窗口不动,仅仅只是旋转一下窗口里的内容,可不可以呢?比如说某个较小窗口里的 Shell 缓冲区将会运行一些长模式的 Shell 命令,我现在希望把它切换到较大的窗口里来,以避免频繁的窗口放大,这样可不可以呢?
实际上这个功能的比起上文所示的窗口旋转要简单的多了。因为窗口不动了,只需要把当前活动缓冲区的名称存储起来,再重新安放一下就行了。下面这个函数就提供了将三窗口布局中的缓冲区进行顺时针旋转的功能。
清单 7. 进行顺时针旋转的功能
; +------------+-----------+ +------------+-----------+ ; | | C | \ | | A | ; | | | +-------+\ | | | ; | A |-----------| +-------+/ | B |-----------| ; | | B | / | | C | ; | | | | | | ; +------------+-----------+ +------------+-----------+ ; ; +------------------------+ +------------------------+ ; | A | \ | B | ; | | +-------+\ | | ; +------------+-----------+ +-------+/ +------------+-----------+ ; | B | C | / | C | A | ; | | | | | | ; +------------+-----------+ +------------+-----------+ (defun roll-v-3 (&optional arg) "Rolling 3 window buffers (anti-)clockwise" (interactive "P") (select-window (get-largest-window)) (if (= 3 (length (window-list))) (let ((winList (window-list))) (let ((1stWin (car winList)) (2ndWin (car (cdr winList))) (3rdWin (car (last winList)))) (let ((1stBuf (window-buffer 1stWin)) (2ndBuf (window-buffer 2ndWin)) (3rdBuf (window-buffer 3rdWin))) (if arg (progn ; anti-clockwise (set-window-buffer 1stWin 3rdBuf) (set-window-buffer 2ndWin 1stBuf) (set-window-buffer 3rdWin 2ndBuf)) (progn ; clockwise (set-window-buffer 1stWin 2ndBuf) (set-window-buffer 2ndWin 3rdBuf) (set-window-buffer 3rdWin 1stBuf)) )))))) |
通过把这个函数绑定在 C-x 4 r
组合键上,就可以方便、快捷的在让窗口里面的内容进行顺时针旋转,同时保持窗口布局不变。如果你想要逆时针旋转的话,只需要在组合键前面加上一个任意的数字前缀 ( C-u 1
或者 M-2
) 就行了。
(global-set-key (kbd "C-x 4 r") (quote roll-v-3)) |
在上文的 让 Emacs 自己选择旋转一节中,细心的读者可能注意到了 change-split-type-3
函数并没有把 split-v-3
和 split-h-3
的代码简单的合并在一个 if else
结构里面,事实上在 change-split-type-3
函数里面和那两个函数一样,都只有一套清理环境并且生成新窗口的代码。
清单 8. 代码
(delete-other-windows) (funcall split-1) (set-window-buffer nil 2ndBuf) (funcall split-2) (set-window-buffer (next-window) 3rdBuf) (other-window 2) (set-window-buffer nil 1stBuf))) |
为什么用一套代码可以完成两件不同的任务呢?这个就是 Lisp 语言的强大的特性之一。在 Lisp 语言当中 函数与 数据这两种对象被使用一种的数据结构 (list
) 来表示和存储,由此使得函数可以像数据一样作为参数在函数间传递。换句话说,Lisp 语言当中的函数除了可以接受数据作为参数以外,还可以接受 指令( 函数 ) 作为 参数。
基于这个特性,我们就可以对所执行的任务在更高的逻辑层次上进行抽象。例如上文所述的两种转换窗口布局的任
务在这个层次上被抽像为下面的任务:
1. 删除当前窗口之外的所有窗口
2. 对当前窗口进行 第一次切分
3. 设置窗口一的缓冲区
4. 对当前窗口进行 第二次切分
5. 设置窗口二的缓冲区
6. 跳至下一个窗口
7. 设置最后一个窗口的缓冲区
这个时候,两件不同的任务就变成了 同一类任务了。唯一的区别仅仅只是 第一次切分和 第二次切分的时候究竟是竖着切还是横着切。这一点点区别就交给 指令去处理了。传给它什么样的 切分指令,它就会按照什么样的指令进行切分。这样不仅极大的简化了函数的代码,而且使函数的逻辑更加接近于现实世界中的行为方式了。
在今天,这样的语言特性已经不是 Lisp 独有了。但是实现这些特性的前提都是要能够用相同的数据结构来表示函数与数据。例如在 Perl 语言当中通过使用对函数的 引用( reference ),来实现指令的传递。因为引用和普通数据一样,在 Perl 语言当中都是 scalar数据结构。
1. 以上所述的多窗口工作方式即使是在字符终端的环境下依然可以工作。因为 GNU Emacs 的窗口功能要远早于 X Window 的出现,所以不依赖于 X Window 的存在。
2. 上文代码中的示意图是用 GNU Emacs 的 artist-mode 绘制的。当你启动这种模式之后,整个缓冲区就变成了一块画布,以鼠标作笔就可以进行任意的挥洒了。