在 Emacs 中編寫 C/C++ 代碼時,頻繁輸入 `#include <...>` 或 `#include "..."` 既繁瑣又易錯。本文提供一套完整的 Emacs Lisp 配置,實現:**輸入 `inc` + 空格觸發補全 → Minibuffer 顯示系統/項目頭文件列表 → TAB 補全文件名 → 自動判斷 `<>` 或 `""` 包裹 → 支持子目錄遞歸補全**。基於 `abbrev-mode` + `skeleton-mode` + 動態補全表構建。
/* 操作流程 */
inc␣ ; 輸入 inc 並按空格 (abbrev 展開)
Include File: stdio.h␣ ; Minibuffer 提示,輸入 stdio 後 TAB 補全 stdio.h,回車
→ #include ; 自動判斷為系統頭文件,用 <> 包裹
inc␣
Include File: mylib/␣ ; 輸入目錄名 + 斜槠 /
→ 列表自動更新顯示 mylib/ 下文件
Include File: mylib/config.h␣
→ #include "mylib/config.h" ; 相對路徑/項目內文件自動用 "" 包裹
;;; === 1. 定義 abbrev 觸發器:inc → skeleton-include ===
(mapc
(lambda (mode)
(define-abbrev-table mode
'(("inc" "" skeleton-include 1)))) ; 1 表示展開後保留點在 minibuffer
'(c-mode-abbrev-table c++-mode-abbrev-table))
;;; === 2. 動態獲取編譯器標準包含路徑 (需 CEDET/semantic 支持) ===
(defconst yc/inc-dir-list
(append (semantic-gcc-get-include-paths "c++") '("./"))
"C/C++ 頭文件搜索路徑列表:系統路徑 + 當前目錄")
(defvar inc-minibuffer-compl-list nil "動態補全候選列表")
;;; === 3. 核心骨架:生成 #include 並處理補全邏輯 ===
(define-skeleton skeleton-include
"Generate #include with smart completion"
""
"> \"#include |XXX|" ; 佔位符 |XXX| 稍後替換為 <> 或 ""
(completing-read ; 彈出 Minibuffer 讀取文件名
"Include File: "
(mapcar ; 構建初始候選列表
(lambda (f) (list f))
(apply 'append
(mapcar
(lambda (dir)
(directory-files dir nil "\\(\\.h\\)?$")) ; 只列 .h 文件
yc/inc-dir-list)))) ; 遍歷所有搜索路徑
"|XXX|"
(yc/update-inc-marks)) ; 後處理:根據路徑決定 <> 或 ""
;;; === 4. Minibuffer 斜槠鍵綁定:輸入 / 時動態展開子目錄 ===
(defun yc/update-minibuffer-complete-table ()
"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)
(unless (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 "/")) ; 實際插入斜槠字符
;; 綁定 / 鍵到上述函數 (僅在 minibuffer 補全模式生效)
(let ((map minibuffer-local-completion-map))
(define-key map "/" 'yc/update-minibuffer-complete-table))
;;; === 5. 後處理:根據文件路徑自動決定包裹符號 ===
(defconst yc/re-include
(rx "#include" (+ ascii) "|XXX|" (group (+ ascii)) "|XXX|"))
(defun yc/update-inc-marks ()
"將佔位符 |XXX| 替換為 <> 或 \"\""
(let ((statement (buffer-substring-no-properties (point-at-bol) (point-at-eol)))
(inc-file nil)
(to-begin nil)
(to-end nil))
(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))))
(provide 'yc-include)
;;; yc-include.el 結束
| 技術點 | 實現細節 | 優勢 |
|---|---|---|
| 系統路徑自動獲取 | `semantic-gcc-get-include-paths "c++"` 查詢 GCC/Clang 內建路徑 | 無需硬編碼 `/usr/include` 等,適配不同工具鏈/版本 |
| 子目錄遞歸補全 | Minibuffer `/` 鍵觸發 `yc/update-minibuffer-complete-table` 掃描子目錄並追加候選 | 無需預先掃描全盤,按需動態加載,支援任意深度 |
| 智能符號判斷 | 補全後 `yc/update-inc-marks` 檢查 `./file.h` 是否存在決定 `<>` vs `""` | 符合 C/C++ 約定:系統頭文件 `<>`,項目頭文件 `""`,零配置 |
| 佔位符技巧 | Skeleton 中用 `|XXX|` 佔位,後處理統一替換 | 避免在 completing-read 階段就決定符號,靈活分離關注點 |
| 過濾 .h 文件 | `directory-files dir nil "\\(\\.h\\)?$"` 正則只列頭文件 | 減少噪音,加速補全響應 |