Vim 下使用 Slimv(类似Slime) 轻松调试 Common Lisp
目录
前情回顾
在上一篇文章想法验证:超轻量级全功能纯文本界面 REPL 类语言 IDE: Vim+Tmux+Slimv中我们对于在 tmux
中新建窗口运行 swank
服务端的想法经过了手工验证, 证明了我们的想法是可行的, 本文则完成实际的配置, 把所有的配置信息都写入到配置文件中.
安装slimv
如果你看过上一篇文章并且照着做了, 那你的 slimv
已经安装好了. 如果没有安装的话可以按照下面的操作来安装:
我们继续通过 pathogen
来管理 slimv
插件, 也就是说, 只要进入 .vim/bundle/
目录下, 把 slimv
用 git
克隆进去, 暂时不做配置, 因为我们要手动进行试验, 安装命令如下:
cd ~/.vim/bundle/
git clone
安装就这么简单, 如果对于 pathogen
的安装有不清楚的地方可以看看本系列第一篇文章里的描述超轻量级纯文本界面 REPL 类语言 IDE.
接下来就是数据配置了.
数据配置
Linux 和 OSX 平台下的配置
主要是对 vim
的配置, 因为我们要从vim
中启动 slimv
和 swank
, 只要在你原来的 .vimrc
配置文件中加入如下内容即可:
"Set mapleader
let mapleader = ","
" slimv for clisp
let g:slimv_swank_cmd = '! tmux new-window -d -n REPL-CLISP "clisp -i ~/.vim/bundle/slimv/slime/start-swank.lisp"'
上面的配置调用了 clisp
, sbcl
和 ccl
的配置分别为:
" slimv for sbcl
let g:slimv_swank_cmd = '! tmux new-window -d -n REPL-SBCL "sbcl --load ~/.vim/bundle/slimv/slime/start-swank.lisp"'
" slimv for ccl
let g:slimv_swank_cmd = '! tmux new-window -d -n REPL-CCL "ccl -l ~/.vim/bundle/slimv/slime/start-swank.lisp"'
Win32 平台下的配置
在 Win32
下不使用 tmux
, 直接按照官方配置文档的配置方法配置就可以了, 需要正确指定两个目录, 一个是 Common Lisp
安装目录, 一个是 slimv
安装目录
- Common Lisp 安装目录:
c:/Program Files/Lisp Cabinet/bin/ccl/
- Slimv 安装目录:
c:/Program Files/Lisp Cabinet/site/lisp/slime
配置命令如下:
let g:slimv_swank_cmd = '!start "c:/Program Files/Lisp Cabinet/bin/ccl/wx86cl.exe" -l "c:/Program Files/Lisp Cabinet/site/lisp/slime/start-swank.lisp"'
还有一个好消息就是我们对 slimv
的数据配置跟原来的 vim-slime
的配置不冲突, 所以这两个插件的功能你可以同时使用, 只要通过不同的快捷键调用即可.
默认快捷键
官网的文档里给出了如下快捷键:
----
*slimv-keyboard*
有两套快捷键可供选择, 默认快捷键绑定第一套, 设置变量为:
g:slimv_keybindings=1
第二套设置变量为:
g:slimv_keybindings=2
注意前导键 <leader> 默认设置为逗号(,), 当然你可以改为其他键, 设置全局变量为:
g:slimv_leader
在图形界面下每个菜单项都会列出对应的快捷键, 不过我们是文本界面, 就没有这个福利了.
vim 定义了快捷键序列的超时值, 如果你觉得自己手速慢, 来不及在规定的超时时间内输完长长的快捷键序列, 那么你也可以自己设置一下相关的超时时间, 直接设置 vim 里对应的这几个参数好了:
timeout
ttimeout
timeoutlen
ttimeoutlen
下面就是具体的快捷键了, Set#1 代表第一套, Set#2 代表第二套, Command 代表该快捷键对应的命令.
Set#1 Set#2 Command
---------------------------------------------------
,, ,, Slimv 菜单
编辑命令 (Insert 模式):
<C-X>0 关闭形式
<Tab> 自动补全输入的符号
<Space> 函数参数列表
编辑命令 (Normal 模式):
,) ,tc 关闭形式
,( ,(t 括号自动成对开关
求值命令:
["x],d ["x],ed 求值 Defun (当前顶层) [放到寄存器 register x]
["x],e ["x],ee 求值当前表达式 (当前子形式) [放到寄存器 reg. x]
["x],r ["x],er 求值区域 (visual 选择) [或者来自 register x 的文本]
,b ,eb 求值缓冲区内所有内容
,v ,ei 交互求值 (evaluates in frame when in SLDB)
,u ,eu 未定义函数求值
调试命令:
,1 ,m1 Macroexpand-1
,m ,ma Macroexpand All
,t ,dt Toggle Trace
,T ,du Untrace All
,B ,db Set Breakpoint
,l ,dd Disassemble
,i ,di Inspect (inspects in frame when in SLDB)
,a ,da Abort
,q ,dq Quit to Toplevel
,n ,dc Continue
,H ,dl List Threads
,K ,dk Kill Thread
,G ,dg Debug Thread
编译命令:
,D ,cd Compile Defun
,L ,cl Compile and Load File
,F ,cf Compile File
["x],R ["x],cr Compile Region [or text from register x]
交叉引用命令:
,xc ,xc Who Calls
,xr ,xr Who References
,xs ,xs Who Sets
,xb ,xb Who Binds
,xm ,xm Who Macroexpands
,xp ,xp Who Specializes
,xl ,xl List Callers
,xe ,xe List Callees
性能测量命令:
,p ,pp Toggle Profile
,B ,pb Profile by Substring
,U ,pa Unprofile All
,? ,ps Show Profiled
,o ,pr Profile Report
,x ,px Profile Reset
文档命令:
,s ,ds Describe Symbol
,A ,da Apropos
,h ,dh Hyperspec
,] ,dt Generate Tags
Repl 命令:
,c ,rc Connect to Server
,y ,ri Interrupt Lisp Process
Set#1 Set#2 Command
---------------------------------------------------
,\ ,\ REPL 菜单 (独立菜单, 仅在 REPL 缓冲区有效)
REPL 菜单命令:
,. ,rs Send Input
,/ ,ro Close and Send Input
,g ,rp Set Package
<C-C> <C-C> Interrupt Lisp Process
,<Up> ,rp Previous Input
,<Down> ,rn Next Input
,- ,- Clear REPL
这些命令确实丰富, 基本上调试程序是够用了, 实在不够用的话也可以在 REPL
区自己手动输入相关的调试命令, 稍微麻烦一些而已.
剩下的部分我们用一个实例研演示一下这个超轻量级全功能纯文本界面的 REPL
开发环境有哪些功能, 主要参考自 slimv
的官方教程一, 二, 三:
Slimv Tutorial - Part One Slimv Tutorial - Part Two Slimv Tutorial - Part Three
实战演练
REPL 区基本操作
首先要说说 paredit
这个插件的效果, 它默认是打开的, 最简单就是自动为你补全括号, 比如你输入一个左括号 (
, 它会自动补全一个右括号 )
, 控制开关在这里, .vimrc
中增加:
let g:paredit_mode=0
它还有个功能叫 electric return
, 当你输入回车时, 会插入新行, 控制开关如下:
let g:paredit_electric_return=0
其次是 Common Lisp
中的几个快捷键, 在 REPL
区使用:
*
, **
, ***
:
可以得到 REPL
区上一次对象求值结果
+
, ++
, +++
:
可以得到 REPL
区上一次求值的形式(表达式)
效果如下:
90 CL-USER> (+ 123 345)
91 468
92 CL-USER> *
93 468
94 CL-USER> **
95 468
96 CL-USER> ***
97 468
98 CL-USER> +
99 ***
100 CL-USER> ++
101 ***
102 CL-USER> (+ 123 345)
103 468
104 CL-USER> +
105 (+ 123 345)
106 CL-USER> ++
107 (+ 123 345)
108 CL-USER>
REPL =============
另外也支持历史命令查看: 可以在 Insert
模式下使用上下键
编辑源文件
开启彩虹括号
先在 ~/.vimrc
配置文件中打开 rainbow-parentheses
, 用下面这条语句:
let g:lisp_rainbow=1
这样你在编辑 lisp
文件时就会发现你的不同层次的括号对会呈现不同的颜色, 同一层次的括号对会使用相同的颜色, 效果是这样:
开始编辑代码
用 vim
建立一个新文件, 命令如下:
vi ~/code-staff/morse.lisp
下面这是一个典型的显示界面, 上半部分是 REPL
区, 下半部分是编辑区:
3 CL-USER>
~
~
~
REPL
1 (defpackage :morse
2 (:use :common-lisp)
3 )
4
5 (in-package :)
~
~
~/code-staff/morse.lisp [+]
(in-package PACKAGE-NAME)
[0] 1:bash 2:vim* 3:bash 4:bash 5:bash- 6:REPL-CLISP "Air.local" 19:45 31- 8-15
最下面一行[0] 1:bash 2:vim* 3:bash 4:bash 5:bash- 6:REPL-CLISP "Air.local" 19:45 31- 8-15
是 tmux
的显示信息; 倒数第二行 (in-package PACKAGE-NAME)
是把光标放在编辑区的 in-package
上时 vim
给出的函数参数信息提示; 倒数第三行 ~/code-staff/morse.lisp [+]
是 vim
状态栏显示的编辑区信息.
截图如下:
文档命令
把光标移动到 defpackage
上, 进入命令模式:
-
输入
,s
, 就可以查看defpackage
的详细的信息; -
输入
,h
打开默认浏览器, 查看HyperSpec
中对defpackage
的定义; -
输入
,A
在REPL
区调用(apropos "defpackage")
; -
输入
,]
增加标签(需要事先安装好ctags
插件)
执行 ,A
的结果:
10 CL-USER> (apropos "defpackage")
11 DEFPACKAGE macro
12 COMMON-LISP::DEFPACKAGE-MODERNIZE
13 COMMON-LISP::DEFPACKAGE-RECORD-SYMNAME
14 ; No value
15 CL-USER>
执行 ,s
的结果:
~/code-staff/morse.lisp [+]
DEFPACKAGE is the symbol DEFPACKAGE, lies in #<PACKAGE COMMON-LISP>, is accessible in 19 packages CLOS, COMMON-LISP, COMMON-LISP-USER,
EXPORTING, EXT, POSIX, PXREF, REGEXP, SCREEN, SWANK, SWANK-LOADER, SWANK-MONITOR, SWANK-REPL, SWANK/BACKEND, SWANK/CLISP, SWANK/GRAY,
SWANK/MATCH, SWANK/RPC, SYSTEM, names a macro, has 1 property SYSTEM::DOCHTTP/1.1 404 Not FoundHTTP/1.1 200 OK
.
ANSI-CL Documentation is at
"http://www.ai.mit.edu/projects/iiip/doc/CommonLISP/HyperSpec/Body/mac_defpackage.html"HTTP/1.1 301 Moved PermanentlyHTTP/1.1 200 OK
CLISP Documentation is at
"http://clisp.cons.org/impnotes/pack-intro.html#defpack"
For more information, evaluate (SYMBOL-PLIST 'DEFPACKAGE).
#<PACKAGE COMMON-LISP> is the package named COMMON-LISP. It has 2 nicknames LISP, CL.
It imports the external symbols of 1 package CLOS and exports 978 symbols to 18 packages SWANK-REPL, SWANK, SWANK/RPC, SWANK/MATCH,
SWANK/GRAY, SWANK/CLISP, SWANK-MONITOR, PXREF, SWANK/BACKEND, SWANK-LOADER, REGEXP, POSIX, EXPORTING, SCREEN, CLOS, COMMON-LISP-USER, EXT,
SYSTEM.
#<MACRO #<COMPILED-FUNCTION DEFPACKAGE> (&WHOLE SYSTEM::WHOLE-FORM SYSTEM::PACKNAME &REST SYSTEM::OPTIONS)> is a macro expander.
Argument list: (&WHOLE SYSTEM::WHOLE-FORM SYSTEM::PACKNAME &REST SYSTEM::OPTIONS)
For more information, evaluate (DISASSEMBLE (MACRO-FUNCTION 'DEFPACKAGE)).
Documentation:
SYSTEM::IMPNOTES:
"pack-intro.html#defpack"
CLHS:
"Body/mac_defpackage.html"
SYSTEM::FILE:
((SYSTEM::DEFUN/DEFMACRO
#P"/opt/local/var/macports/build/_opt_local_var_macports_sources_rsync.macports.org_release_tarballs_ports_lang_clisp/clisp/work/clisp-2.49/s
rc/defpackage.fas"
11 202))
Press ENTER or type command to continue
求值命令
现在开始调试我们的小程序, 先求值第一个形式, 把光标放在 (defpackage ...)
形式体内任何地方, 然后输入 ,d
, 我们的这个形式体就被发送到 REPL
并且完成求值, 这个命令会求值顶层的形式, 结果如下:
15 CL-USER> (defpackage :morse
16 (:use :common-lisp)
17 )
18 #<PACKAGE MORSE>
19 CL-USER>
现在再把光标移到 (in-package ...)
内并且输入 ,e
, 这个命令将会在 REPL
缓冲区求值当前的 S-表达式
, 结果如下:
19 CL-USER> (in-package :morse)
20 #<PACKAGE MORSE>
21 CL-USER>
我们可以在 REPL
输入 (find-package :morse)
来检查是否生效, 还可以输入 ,g
把当前包设置为 :morse
并且进入该包, 这个改变会通过 REPL
提示符的变化(变为 morse
)以及变量 *package*
反映出来:
30 #<PACKAGE MORSE>
31 CL-USER>
32 MORSE>*package*
33 #<PACKAGE MORSE>
34 MORSE>
现在让我们加入莫尔斯码映射函数, 在我们输入 defparameter
和 空格
后, 函数参数列表会出现在状态栏. 这个功能对于所有用户定义的函数也有效, 不仅仅是那些内建函数.
5 (in-package :morse)
6 (defparameter )
~
~
~/code-staff/morse.lisp [+]
(defparameter &WHOLE WHOLE-FORM SYMBOL INITIAL-VALUE &OPTIONAL DOCSTRING)
[0] 1:bash 2:vim* 3:bash 4:bash 5:bash- 6:REPL-CLISP
现在开始填充莫尔斯映射表, 可以从网络上搜索到, 这里可以对代码进行自动缩进, 选择好区域后按 =
(貌似这里我的表现跟教程的不太一致,先写, 后面再找原因)
5 (in-package :morse)
6 (defparameter *morse-mapping*
7 '((#\A ".-")
8 (#\B "-...")
9 (#\C "-.-.")
10 (#\, "--..--")
11 (#\? "..--..")
12 )
13 )
14
15 (defun character-to-morse (character)
16 (assoc character *morse-mapping* :test #'char-equal)
17 )
18
~
输入单独的左括号
然后我们意识到我们只需要返回值的第二部分, 所以我们需要把 cdr
放在 (assoc ...)
前面,但是因为有 paredit
的缘故,我们无法输入一个单独的左括号 (
, 因为它会自动输入成对的括号 ()
, 把光标移到最前面的左括号, 也就是这个 (assoc
, 然后按下 ,w
或 ,W
(在我们的环境下大写 W
有效, 小写无效), 它会在 S-表达式
外面用一对新括号把表达式括起来(paredit wrap
), 现在我们就可以输入 cdr
了:
13 (defun character-to-morse (character)
14 (cdr (assoc character *morse-mapping* :test #'char-equal)))
paredit wrap
的相反操作是 splice
, 通过按下 ,s
, 它会删除掉最外层的括号, 还有一些类似的命令:
- 输入
,o
切分S-表达式 Split S-expression - 输入
,J
加入S-表达式 Join S-expression - 输入
,I
提升子形式 Raise subform - 输入
,<
括号左移 Move left - 输入
,>
括号右移 Move right
编译
现在编译我们的代码, 输入 ,D
, 我们还可以编译和加载整个代码源文件:
,F
编译整个文件,L
编译并且加载整个文件
结果如下:
42 MORSE>
43
44 Compilation finished. (No warnings) [0.003131000092253089 secs]
45
46 MORSE>
47
48 Compilation finished. (No warnings) [0.002271000063046813 secs]
49
50 MORSE> ;; Compiling file /Users/admin/code-staff/morse.lisp ...
51 ;; Wrote file /Users/admin/code-staff/morse.fas
52 0 errors, 0 warnings
53 MORSE>
54
55 Compilation finished. (No warnings) [0.03273700177669525 secs]
56
57 MORSE>
自动补全
现在是时候测试一下我们的 character-to-morse
函数了, 输入 C-w w
从代码编辑区切换到 REPL
缓冲区, 输入 char
接着按下 Tab
键, 你将会看到弹出一个可能的自动完成列表, 截图如下:
如果你继续输入更多字符, 弹出菜单中的选项会自动缩小范围匹配, 截图如下:
默认调用的方法是模糊补全 fuzzy completion
, 因此你甚至可以输入 ctm
再按 Tab
(ctm 是 character-to-morse 的首字母)
这种补全方法在 REPL
缓冲区也不受限制, 在代码编辑区它以同样的方式工作, 直到 slimv
连接到 swank
服务器上.
顺便说一句, 还有另一种 vim
的自动补全, 通过按 C-p
和 C-n
, 这种补全方式会在当前缓冲区查找相同的单词前缀, 这种方式对于补全那些不是符号名的单词比较有用, 比如注释或者字符串里的某些文本.
- 输入
Ctrl p
向前查找 - 输入
Ctrl n
向后查找
我们选择了正确的补全完成函数调用:
98 MORSE> (character-to-morse #\a)
99 (".-")
100 MORSE> (character-to-morse #\b)
101 ("-...")
102 MORSE> (character-to-morse #\c)
103 ("-.-.")
104 MORSE>
我们得到的是一个列表, 里面包含一个字符串, 但是我们需要的结果是一个字符串, 我们意识到我们应该用 second
来代替函数中的 cdr
, 因此我们相应地修改代码并且按下 ,d
重新求值这个 defun
13 (defun character-to-morse (character)
14 (second (assoc character *morse-mapping* :test #'char-equal)))
切换回 REPL
缓冲区, 在 Insert
模式下按下 向上
箭头来重新调用最后一条命令, 然后输入回车求值:
104 MORSE> (defun character-to-morse (character)
105 (second (assoc character *morse-mapping* :test #'char-equal)))
106 CHARACTER-TO-MORSE
107 MORSE> (character-to-morse #\c)
108 "-.-."
109 MORSE>
好极了, character-to-morse
现在返回了作为参数输入的字符的莫尔斯代码串.
截图不完整, 后面补