Emacs 自动补全头文件
1 目标
- 1: 写 C/C++ 代码时候,可以根据自动补全头文件。 注意,是补全,也就是说至少我们需要输入几个字符让它帮忙补全。
- 2: 如果头文件存放在搜索路径的子目录中,可以自动列出子目录中的文件, 并将这些文件添加到用于补齐的候选名单中。
- 3: 补齐完成之后,可以自动判断到底应该使用 #include <FILE> 还是 #include "FILE" 。
2 方案
emacser.org 上有一篇文章,其中提到了这个解决方法, 主要是利用了 abbrev-mode 和 skeleton-mode 来实现, 通过这个方法,我们输入inc, 然后按空格, 会提示输入文件名称。
代码如下:
;; 安装 abbrev (mapc (lambda (mode) (define-abbrev-table mode '( ("inc" "" skeleton-include 1) ))) '(c-mode-abbrev-table c++-mode-abbrev-table)) ;; 输入 inc , 可以自动提示输入文件名称,可以自动补全. (define-skeleton skeleton-include "generate include<>" "" > "#include <" (completing-read "Include File:" (mapcar #'(lambda (f) (list f )) (apply 'append (mapcar #'(lambda (dir) (directory-files dir)) (list "/usr/include" "/usr/local/include" "/usr/include/g++-3"))))) ">")
该方法有若干局限性:
- 头文件的搜索路径是写死的,如果某个目录不存在,上面的代码会报错,不能补全。
- 无法补全搜索路径的子目录下的文件 ( 即前面的 AIM 2)。
- 没有判断在 #include 一个文件的时候,是应该使用符号 <> 还是符号 "" (即前面的 AIM 3)
- 通过某种方法来从系统中自动获取 include 的搜索路径。
比如 CEDET 提供的: semantic-gcc-get-include-paths 函数。
- 重定义 minibuffer-mode 下的按键 "/"
将其绑定到一个用于搜索和展开某个目录,并更新 minibuffer-completion-table 的函数 (minibuffer-completion-table 是 minibuffer-mode 中补全的候选 list)。 - 在 skeleton-include 中不使用 <> 或者 "
我们可以使用一个特殊的标记,然后在 skeleton-include 的结尾,根据头文件的路径判断到底应该使用什么符号。
;; 输入 inc , 可以自动提示输入文件名称,可以自动补全. (mapc (lambda (mode) (define-abbrev-table mode '( ("inc" "" skeleton-include 1) ))) '(c-mode-abbrev-table c++-mode-abbrev-table)) (defconst yc/inc-dir-list (append (semantic-gcc-get-include-paths "c++") '("./")) "nil") (defvar inc-minibuffer-compl-list nil "nil") (defun yc/update-minibuffer-complete-table ( ) "Complete minibuffer" (interactive) (let ((prompt (minibuffer-prompt)) (comp-part (minibuffer-contents-no-properties))) (when (and (string= "Include File:" prompt) (> (length comp-part) 0)) (setq minibuffer-completion-table (append minibuffer-completion-table (let ((inc-files nil) (dirname nil) (tmp-name nil)) (mapc (lambda (d) (setq dirname (format "%s/%s" d comp-part)) (when (file-exists-p dirname) (mapc (lambda (x) (when (not (or (string= "." x) (string= ".." x))) (setq tmp-name (format "%s/%s" comp-part x)) (add-to-list 'inc-files tmp-name))) (directory-files dirname)))) yc/inc-dir-list) inc-files))))) (insert "/")) (let ((map minibuffer-local-completion-map)) (define-key map "/" 'yc/update-minibuffer-complete-table)) (defun yc/update-inc-marks ( ) "description" (let ((statement (buffer-substring-no-properties (point-at-bol) (point-at-eol))) (inc-file nil) (to-begin nil) (to-end nil) (yc/re-include (rx "#include" (+ ascii) "|XXX|" (group (+ ascii)) "|XXX|"))) (when (string-match yc/re-include statement) (setq inc-file (match-string 1 statement)) (if (file-exists-p (format "./%s" inc-file)) (setq to-begin "\"" to-end "\"") (setq to-begin "<" to-end ">") ) (move-beginning-of-line 1) (kill-line) (insert (format "#include %s%s%s" to-begin inc-file to-end)) (move-end-of-line 1)))) (define-skeleton skeleton-include "generate include<>" "" > "#include |XXX|" (completing-read "Include File:" (mapcar (lambda (f) (list f )) (apply 'append (mapcar (lambda (dir) (directory-files dir nil "\\(\\.h\\)?")) yc/inc-dir-list)))) "|XXX|" (yc/update-inc-marks))
3 使用和效果
使用方法很简单:
- 1: 将上述的代码添加到 Emacs 的配置文件后,打开一个 C/C++ 程序,
- 2: 输入 inc 然后按下空格,然后在 minibuffer 中输入部分头文件的名字,并通过 TAB 来补全。
- 3: 如果头文件位于子目录中,则输入目录名后输入 "/" 。
这样子目录中的内容也会添加到补齐的候选名单中,然后就又可以继续他过 TAB 补全了。
- 4: 确认 minibuffer 中填写的内容无误后,回车, skeleton-include 将自动更新标记符号。
下面是几张截图:
Include 系统文件
Include 自定义文件
全部完成后截图