Emacs——神的编辑器入门教程

· · 科技·工程

更好的食用体验

引言

Emacs 是神的编辑器,Vim 是编辑器的神。

常闻 Emacs 与 Vim 齐名,这个长期处于编辑器鄙视链顶端的编辑器究竟有何魅力?或许用上它之后你就明白了。

Emacs 是什么?

一个可扩展、可自定义、免费自由的文本编辑器。

为什么要学习 Emacs?

快捷

Emacs 将快捷键使用到极致,凡事都用快捷键解决。

可扩展,可定制

这是 Emacs 最强调竞争力。由于 Emacs 没有因为安全因素对用户作出任何限制,所以没有什么是扩展不出来的。网上有大量插件可以下载,几乎没有你下载不到的,只有你根本想不到的。另外 Emacs 自身使用 Elisp 语言编写,虽然有些晦涩难懂,但是学会之后你就能自由地改造任何地方。

豪不夸张地说:一千个人的电脑中有一千个 Emacs。

以上这两点虽然是 Emacs 的优点,同时却也增加了其学习难度。网上有一张有趣的图片:

安装

参照 GNU Emacs 官网。

这里给出 ubuntu 系统的 Emacs 安装命令:

sudo apt-get install emacs

还有 windows 系统的 Emacs下载链接。如果下载太慢,可以用阿里云镜像下载。

注意,命令安装的 Emacs 通常版本比较旧(比如上面的命令目前会安装 Emacs29.3),如果你想要体验新版(Emacs30.1,Emacs30.2)可以手动编译安装。

本文环境为 ubuntu 下的 Emacs29.3。

启动 Emacs

Emacs 有图形界面版本和终端版本,在终端输入

emacs

即可打开图形界面版本。

emacs -nw

即可打开终端版本。 这两条命令后面加文件名即可打开文件,若文件不存在则会自动创建。

认识 Emacs 界面

emacs 初次打开后应该长这样:

下面介绍一些基本概念。

基本操作

基本快捷键

为方便叙述,以后用 C 代表 Ctrl 键;M 代表 Alt 键;C-?M-? 表示同时按下 C?M?C-M-? 表示同时按下 CM?? ? 表示依次按下前后两个键。

打开一个目录:C-x d

打开一个文件(不存在会自动创建):C-x C-f

保存文件:C-x C-s

关闭 Emacs:C-c C-x

window 管理

横向分屏:C-x 3

纵向分屏:C-x 2

只保留光标所在分屏,删除其他 window:C-x 1

删除光标所在 window:C-x 0

切换到下一个 window:C-x o

另外,用鼠标可以调整 window 大小。

buffer 管理

切换 buffer:C-x b

上/下一个 buffer:C-x <left>/<right>

杀死 buffer:C-x k

命令

按下 M-x 可以输入命令,按下 C-g 可以放弃当前命令。很多命令可以用快捷键替代。

基本配置

启动 Emacs 时,Emacs 会自动依次寻找以下几个文件之一作为配置文件(一旦找到了其中之一,就不会继续按顺序寻找后面的其它文件了):

~/.emacs
~/.emacs.el
~/.emacs.d/init.el
~/.config/emacs/init.el

一般习惯把配置文件放在 ~/.emacs.d/init.el,配置越来越多之后可以分门别类地放在 ~/.emacs.d 的其他文件中,并在 init.el 中加载它们。

下面,在 init.el 中写入:

(setq-default inhibit-startup-screen t) ;;不显示欢迎页面
(menu-bar-mode -1) ; 关闭 menubar
(tool-bar-mode -1) ; 关闭 toolbar
(when (display-graphic-p) (toggle-scroll-bar -1)) ; 图形界面时关闭滚动条

;; 只在编程模式下显示行号
(add-hook 'prog-mode-hook 'display-line-numbers-mode)
(defun turn-off-line-numbers ()        ; 在其他模式下关闭行号
  "关闭行号显示"
  (display-line-numbers-mode -1))
(dolist (mode '(text-mode-hook         ; 为一些不需要行号的模式添加钩子
                fundamental-mode-hook
                messages-buffer-mode-hook
                shell-mode-hook
                term-mode-hook
                eshell-mode-hook
                dired-mode-hook
                help-mode-hook
                info-mode-hook))
  (add-hook mode 'turn-off-line-numbers))

(global-auto-revert-mode t) ; 当另一程序修改了文件时,让 Emacs 及时刷新 Buffer
(setq make-backup-files nil) ; 关闭文件自动备份
(setq create-lockfiles nil) ; 不锁文件

(setq-default kill-ring-max 65535) ; 扩大可撤销记录
(delete-selection-mode t) ; 选中文本后输入文本会替换文本
(electric-pair-mode t) ; 自动补全括号

(add-hook 'prog-mode-hook #'hs-minor-mode) ; 编程模式下,可以折叠代码块

;; 设置缩进
(setq-default c-basic-offset 2) ; 个人习惯缩进两格,也可以调成 4
(setq-default indent-tabs-mode nil) ; 用空格缩进
(setq-default default-tab-width 2)
(setq-default tab-width 2)

;; 设置默认编码环境
(set-language-environment "UTF-8")
(set-default-coding-systems 'utf-8)

配置保存好后可以按 C-c C-e 加载,而更彻底的方法是直接重启 Emacs 生效。

字体

我用的是 Fira Code

先安装(ubuntu):

sudo apt install fonts-firacode

windows 可以从 Fira Code 的 github 官网 安装。

接下来在 init.el 中启用字体,并开启连字功能:

(when (window-system)
  (set-frame-font "Fira Code-16"))
(let ((alist '((33 . ".\\(?:\\(?:==\\|!!\\)\\|[!=]\\)")
               (35 . ".\\(?:###\\|##\\|_(\\|[#(?[_{]\\)")
               (36 . ".\\(?:>\\)")
               (37 . ".\\(?:\\(?:%%\\)\\|%\\)")
               (38 . ".\\(?:\\(?:&&\\)\\|&\\)")
               (42 . ".\\(?:\\(?:\\*\\*/\\)\\|\\(?:\\*[*/]\\)\\|[*/>]\\)")
               (43 . ".\\(?:\\(?:\\+\\+\\)\\|[+>]\\)")
               (45 . ".\\(?:\\(?:-[>-]\\|<<\\|>>\\)\\|[<>}~-]\\)")
               (46 . ".\\(?:\\(?:\\.[.<]\\)\\|[.=-]\\)")
               (47 . ".\\(?:\\(?:\\*\\*\\|//\\|==\\)\\|[*/=>]\\)")
               (48 . ".\\(?:x[a-zA-Z]\\)")
               (58 . ".\\(?:::\\|[:=]\\)")
               (59 . ".\\(?:;;\\|;\\)")
               (60 . ".\\(?:\\(?:!--\\)\\|\\(?:~~\\|->\\|\\$>\\|\\*>\\|\\+>\\|--\\|<[<=-]\\|=[<=>]\\||>\\)\\|[*$+~/<=>|-]\\)")
               (61 . ".\\(?:\\(?:/=\\|:=\\|<<\\|=[=>]\\|>>\\)\\|[<=>~]\\)")
               (62 . ".\\(?:\\(?:=>\\|>[=>-]\\)\\|[=>-]\\)")
               (63 . ".\\(?:\\(\\?\\?\\)\\|[:=?]\\)")
               (91 . ".\\(?:]\\)")
               (92 . ".\\(?:\\(?:\\\\\\\\\\)\\|\\\\\\)")
               (94 . ".\\(?:=\\)")
               (119 . ".\\(?:ww\\)")
               (123 . ".\\(?:-\\)")
               (124 . ".\\(?:\\(?:|[=|]\\)\\|[=>|]\\)")
               (126 . ".\\(?:~>\\|~~\\|[>=@~-]\\)")
               )
             ))
  (dolist (char-regexp alist)
    (set-char-table-range composition-function-table (car char-regexp)
                          `([,(cdr char-regexp) 0 font-shape-gstring]))))

如果你用其他字体可以参照这条:

(set-face-attribute 'default nil :font "Ubuntu Mono-16")

其中 Ubuntu Mono换为你想要的字体,16 可以控制字体大小,按需调整。

init.el 结尾,要有一句

(provide 'init)

快捷键配置

为避免过于杂乱,我们把快捷键配置放在 ~/.emacs.d/init/key-bindings.el 文件中。

~/.emacs.d/init/key-bindings.el 中写入:

(cua-mode t) ; C-c/v/x 复制/粘贴/剪切
(global-set-key (kbd "C-a") 'mark-whole-buffer) ; 全选快捷键
(global-set-key (kbd "C-z") 'undo) ; 撤销快捷键
(global-set-key (kbd "C-s") 'save-buffer) ; 保存快捷键

(defun insert-line-below-simple ()
  (interactive)
  (end-of-line)
  (newline-and-indent))

(defun insert-line-above-simple ()
  (interactive)
  (beginning-of-line)
  (newline-and-indent)
  (forward-line -1)
  (indent-according-to-mode))

;; 在 cua-mode 加载后覆盖绑定
(with-eval-after-load 'cua-base
  ;; 移除 cua-mode 原有的绑定
  (define-key cua-global-keymap (kbd "C-<return>") nil)
  (define-key cua-global-keymap (kbd "C-S-<return>") nil)

  (global-set-key (kbd "C-<return>") 'insert-line-below-simple)
  (global-set-key (kbd "C-S-<return>") 'insert-line-above-simple))

(defun tab-or-insert-spaces ()
  "在行首缩进,在行中插入空格。"
  (interactive)
  (if (bolp) ;; 判断光标是否在行首 (Beginning Of Line)
      (indent-for-tab-command) ;; 在行首,执行缩进
    (insert "  "))) ;; 不在行首,插入两个空格(你也可以按习惯调成 4 个空格)
(global-set-key (kbd "TAB") 'tab-or-insert-spaces)

;; 更人性化的 Ctrl + Backspace/Delete
(defun smart-backward-kill ()
  (interactive)
  (let ((pos (point)))
    (skip-chars-backward " \t\n\r\f")
    (if (= (point) pos)
        (let ((start (point)))
          (cond
           ((looking-back "[[:punct:]]+" nil)
            (while (and (> (point) (point-min))
                        (looking-back "[[:punct:]]" nil))
              (backward-char)))
           (t
            (backward-word 1)))
          (delete-region (point) start))
      (delete-region (point) pos))))
(defun smart-forward-kill ()
  (interactive)
  (let ((pos (point)))
    (skip-chars-forward " \t\n\r\f")
    (if (= (point) pos)
        (let ((end (point)))
          (cond
           ((looking-at "[[:punct:]]+")
            (while (and (< (point) (point-max))
                        (looking-at "[[:punct:]]"))
              (forward-char)))
           (t
            (forward-word 1)))
          (delete-region end (point)))
      (delete-region pos (point)))))
(global-set-key (kbd "C-<backspace>") 'smart-backward-kill)
(global-set-key (kbd "C-<delete>") 'smart-forward-kill)

;; 更人性化的 Ctrl + <- / ->
(defun smart-backward-move ()
  (interactive)
  (let ((pos (point)))
    (skip-chars-backward " \t\n\r\f")
    (if (= (point) pos)
        (let ((start (point)))
          (cond
           ((looking-back "[[:punct:]]+" nil)
            (while (and (> (point) (point-min))
                        (looking-back "[[:punct:]]" nil))
              (backward-char)))
           (t
            (backward-word 1)))
          )
      )))
(defun smart-forward-move ()
  (interactive)
  (let ((pos (point)))
    (skip-chars-forward " \t\n\r\f")
    (if (= (point) pos)
        (let ((end (point)))
          (cond
           ((looking-at "[[:punct:]]+")
            (while (and (< (point) (point-max))
                        (looking-at "[[:punct:]]"))
              (forward-char)))
           (t
            (forward-word 1)))
          )
      )))
(global-set-key (kbd "C-<left>") 'smart-backward-move)
(global-set-key (kbd "C-<right>") 'smart-forward-move)

;; 更人性化的 Ctrl + Shift + <- / ->
(defun smart-backward-extend ()
  (interactive)
  (let ((pos (point)))
    (skip-chars-backward " \t\n\r\f")
    (if (= (point) pos)
        (let ((start (point)))
          (cond
           ((looking-back "[[:punct:]]+" nil)
            (while (and (> (point) (point-min))
                        (looking-back "[[:punct:]]" nil))
              (backward-char)))
           (t
            (backward-word 1)))
          ;; 处理选择
          (if (use-region-p)
              (setq deactivate-mark nil)  ; 保持区域激活
            (push-mark start t t)))
      ;; 跳过了空白字符
      (if (use-region-p)
          (setq deactivate-mark nil)
        (push-mark pos t t)))))
(defun smart-forward-extend ()
  (interactive)
  (let ((pos (point)))
    (skip-chars-forward " \t\n\r\f")
    (if (= (point) pos)
        (let ((end (point)))
          (cond
           ((looking-at "[[:punct:]]+")
            (while (and (< (point) (point-max))
                        (looking-at "[[:punct:]]"))
              (forward-char)))
           (t
            (forward-word 1)))
          ;; 处理选择
          (if (use-region-p)
              (setq deactivate-mark nil)
            (push-mark end t t)))
      (if (use-region-p)
          (setq deactivate-mark nil)
        (push-mark pos t t)))))
(global-set-key (kbd "C-S-<left>") 'smart-backward-extend)
(global-set-key (kbd "C-S-<right>") 'smart-forward-extend)

(global-set-key (kbd "C-S-k") 'kill-whole-line)

(global-set-key (kbd "M-f") 'isearch-forward)
(global-set-key (kbd "M-b") 'isearch-backward)

(provide 'key-bindings)

你也可以仿照上面的格式,根据自己的需求自定义快捷键。

一键编译运行

Emacs 自带了 compile 命令,但是这条命令不能运行程序。对于运行,蒟蒻强烈推荐在 async-shell 中进行。

我们可以设计一个函数来一键编译运行,并绑定到 f5 上。

直接写进配置(在 (provide 'key-bindings) 之前):

(defun compile-and-run-in-term ()
  "一键编译运行C++,自动聚焦到输出窗口"
  (interactive)
  (let* ((file (buffer-file-name))
         (dir (file-name-directory file))
         (base-name (file-name-base file))
         ;; 根据平台选择可执行文件名
         (exe-name (if (eq system-type 'windows-nt) 
                       (concat base-name ".exe")
                     base-name))
         ;; 根据平台选择路径分隔符
         (path-sep (if (eq system-type 'windows-nt) "\\" "/"))
         ;; 临时文件路径
         (temp-cpp (format "%s%s%s_temp.cpp" dir path-sep base-name))
         ;; 缓冲区名称
         (buf-name (format "*%s*" base-name)))

    ;; 将当前缓冲区内容写入临时文件(不保存原文件)
    (write-region (point-min) (point-max) temp-cpp nil 'quiet)

    (let ((cmd 
           (cond
            ;; Windows
            ((eq system-type 'windows-nt)
             (format "cd /d %s && g++ -std=c++14 -O2 -Wall -o %s %s_temp.cpp && %s && del %s_temp.cpp && del %s"
                     dir
                     exe-name
                     base-name
                     exe-name
                     base-name
                     exe-name))
            ;; Unix/Linux/macOS
            (t
             (format "cd %s && (g++ -std=c++14 -O2 -Wall -o %s %s_temp.cpp && ./%s) ; rm -f %s_temp.cpp %s"
                     dir
                     exe-name
                     base-name
                     exe-name
                     base-name
                     exe-name)))))

      ;; 如果有旧的运行缓冲区,先删除
      (when (get-buffer buf-name)
        (kill-buffer buf-name))

      ;; 异步执行命令
      (async-shell-command cmd buf-name)

      ;; 等待一下确保缓冲区已创建
      (sit-for 0.1)

      ;; 切换到输出缓冲区
      (when (get-buffer buf-name)
        (switch-to-buffer-other-window buf-name)
        (end-of-buffer)))))

(eval-after-load 'cc-mode
  '(define-key c++-mode-map (kbd "<f5>") 'compile-and-run-in-term))

保存好后,在 init.el 中加入:


(add-to-list 'load-path "~/.emacs.d/init/")
(require 'key-bindings)

这样打开 Emacs 时就会加载这些配置了。

插件配置

我们新建文件 ~/.emacs.d/init/pack.el,后面的插件配置就写在这里。不要忘记在文件最后写上一句 (provide 'pack),并且在 init.el 中写上 (require 'pack)(如果你之前没有在 init.el 里写 (add-to-list 'load-path "~/.emacs.d/init/"),请把这句话补充在 require 之前)。

插件从哪里下载呢?Emacs 最大的插件仓库就是 MELPA 了。配置如下:

(require 'package)
(setq package-archives '(("gnu"   . "http://mirrors.cloud.tencent.com/elpa/gnu/")
                         ("melpa" . "http://mirrors.cloud.tencent.com/elpa/melpa/")))
(package-initialize)

这里用的是腾讯镜像用来加快速度,也可以用清华镜像:

(setq package-archives '(("gnu"   . "https://mirrors.tuna.tsinghua.edu.cn/elpa/gnu/")
                         ("melpa" . "https://mirrors.tuna.tsinghua.edu.cn/elpa/melpa/")))

接下来管理插件可以用 use-package 方便地实现,注意 use-package 从 Emacs29.1 开始集成到 Emacs 中,Emacs28 及以下版本需安装:

(eval-when-compile
  (require 'use-package))

下面介绍第一组插件:vertico 和 orderless

vertico & orderless

vertico 官网

orderless官网

vertico 可以增强 minibuffer 的补全功能,并显示补全菜单。orderless 支持了模糊补全。这两个插件一起配合可以大大增强 Emacs 使用体验。

配置

(use-package vertico
  :ensure t
  :init (vertico-mode t))

(use-package orderless
  :ensure t
  :init (setq completion-styles '(orderless)))

效果

consult

consult 官网

consult 插件提供了强大的搜索功能,以及非常友好的交互。

配置

(use-package consult
  :ensure t
  :config
  (setq consult-fd-args "fd -H -u") ; 不忽视隐藏文件(Windows 用户请删除这一行)
  :bind
  ("C-x b" . consult-buffer)
  ("C-f" . consult-line)
  ("C-c f" . consult-fd) ; windows 用不了,删掉这行
  ("C-c r" . consult-ripgrep))

此时按 C-f 就可以在 buffer 内搜索:

接着,我们想要用 C-c f 来根据文件名搜索文件,C-c r 根据文件内容搜索文件。它们分别有两种选择:

根据名称搜索:

  1. consult-find
  2. consult-fd(windows 用不了)

根据内容搜索:

  1. consult-grep
  2. consult-ripgrep

编号为 1 的两个工具可以直接使用,而编号为 2 的两个工具需要手动安装,不过后者速度几乎碾压前者,因此我们选择后者。

安装 rg

我们前往 ripgrep 官网,在 release 中找到你需要的压缩包下载,然后把解压出来的 rg 文件放在以下目录:

回到 Emacs,consult-ripgrep 即可正常使用。效果如下

安装 fd(windows 用不了)

如果你是 windows 用户,可以考虑用另一个强大的工具 everything 代替。

如果你是 linux 用户,就访问 fd 官网,在 release 中找到你需要的压缩包下载,然后把解压出来的 fd 文件放在 /usr/local/bin/ 目录下。接着回到 Emacs 就可以正常使用了。

此外,插件中的 consult-buffer 还提供了很好用的虚拟 buffer 功能,即可以切换到最近打开而当前未打开的 buffer,如图:

company

company 官网

这个插件的名字可不是“公司”,而是“complete any”的缩写,意为“补全一切”,顾名思义可以提供代码补全。

配置:

(use-package company
  :ensure t
  :init
  (setq company-global-modes '(not shell-mode eshell-mode term-mode))
  (global-company-mode 1)
  :config
  (setq company-minimum-prefix-length 1) ; 只需敲 1 个字母就开始进行自动补全
  :bind (:map company-active-map
         ("<tab>" . company-complete-selection))) ; 按 tab 补全

然后在 .el 文件中,敲一个字母就会发现有补全功能了。

但是想要补全 cpp 文件,还需要下一个插件。

eglot

为实现代码分析功能,我们可以用微软为 VSCode 设计的 LSP(Language Server Protocol)。Emacs 中提供 LSP 的插件有 lsp 和 eglot。前者功能更多,后者更加轻量,且集成于 Emacs29 中,这里选择后者。

(use-package eglot
  :ensure t
  :hook ((c++-mode . eglot-ensure))
  :config
  (add-to-list 'eglot-ignored-server-capabilities :inlayHintProvider)
  (add-to-list 'eglot-server-programs '((c++-mode)
                                        "clangd"
                                        "--header-insertion=never" ; 不自动补全头文件,OIer 狂喜
                                        "--clang-tidy"
                                        "--function-arg-placeholders=0"))
  (setq eglot-autoreconnect nil))

但是,光有 LSP 是不够的,我们还需要下载 LSP 的服务器,对于 C++ 来说可以下载 clangd。另外,gcc 等工具对于 OIer 来说也是必不可少的。

Ubuntu:

sudo apt-get update
sudo apt-get install build-essential gdb clangd

Windows:先安装 msys2 然后在 msys2 中运行

pacman -Syu
pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-gdb mingw-w64-x86_64-clang mingw-w64-x86_64-clang-tools-extra

最后,如果是 linux 系统,还需要在根目录(确保你的项目都在“根目录”即可)下创建一个 .clangd 文件,写入

CompileFlags:
  Add: 
    - "-isystem/usr/include/c++/13"
    - "-isystem/usr/include/x86_64-linux-gnu/c++/13"
  Compiler: g++

如果是 windows 系统,就打开应用“编辑系统环境变量”,然后打开“环境变量”->“Path”->“编辑”->“新建”,写入

C:\msys64\mingw64\bin

再次“新建”,写入

C:\msys64\usr\bin

注意,如果你的安装目录不是默认的 C:\msys64\,就把 C:\msys64\ 换成你的安装目录。

接着依次点击三个“确定”按钮,就生效了。

然后在 cpp 文件中就会发现有补全了。

但是函数补全时不会补全括号,加入以下配置可以让补全更人性化。

;; 使 LSP 补全支持 snippet,从而在函数补全时插入括号
(use-package yasnippet
  :ensure t
  :init (yas-global-mode 1))

;; 让 company 优先使用 capf(eglot)提供的补全,获得函数调用片段
(with-eval-after-load 'company
  (setq company-backends '(company-capf)))

;; 更智能的括号插入:
;; - 若候选为函数:自动插入 ()
;;   - 有参:光标置于括号内
;;   - 无参:光标留在括号外
(defvar-local xtt/company--last-candidate nil)
(defvar-local xtt/company--last-backend nil)

(defun xtt/company--capture-candidate (&rest _)
  (setq xtt/company--last-backend company-backend
        xtt/company--last-candidate (and (boundp 'company-candidates)
                                         (nth company-selection company-candidates))))

(defun xtt/company--function-like-p (cand)
  (let* ((kind (ignore-errors (company-call-backend 'kind cand)))
         (ann  (ignore-errors (company-call-backend 'annotation cand)))
         (kind-func (or (memq kind '(function method constructor))
                        (and (integerp kind) (memq kind '(2 3 6)))))
         (ann-has-parens (and (stringp ann) (string-match-p "(.*)" ann))))
          (or kind-func ann-has-parens)))

(defun xtt/company--no-arg-p (cand)
  (let ((ann (ignore-errors (company-call-backend 'annotation cand))))
    (when (stringp ann)
      (if (string-match "(\([^)]*\))" ann)
          (let* ((inner (match-string 1 ann))
                 (trimmed (string-trim (or inner ""))))
            (or (string-empty-p trimmed)
                (string-equal trimmed "void")))
        nil))))

(defun xtt/company--post-complete-insert-parens (&rest _)
  (let ((cand xtt/company--last-candidate)
        (backend xtt/company--last-backend))
    (setq xtt/company--last-candidate nil
          xtt/company--last-backend nil)
    (when (and cand
               (eq backend 'company-capf)
               (xtt/company--function-like-p cand))
      (let ((no-arg (xtt/company--no-arg-p cand)))
        (insert "()")
        (unless no-arg (backward-char 1))))))

(with-eval-after-load 'company
  (advice-add 'company-complete-selection :before #'xtt/company--capture-candidate)
  (advice-add 'company-complete-selection :after  #'xtt/company--post-complete-insert-parens))

这样补全就更智能了。

flymake

主流的语法检查工具还有 flycheck,因为 eglot 用的是 flymake 所以这里也用 flymake。

配置:

(use-package flymake
  :ensure t
  :hook (prog-mode . flymake-mode))

dashboard

dashboard 官网

这个插件可以提供一个比较好看的启动界面。

配置

(use-package dashboard
  :ensure t
  :config
  (dashboard-setup-startup-hook)
  (setq dashboard-navigation-cycle t)
  (setq dashboard-center-content t)
  (setq dashboard-image-banner-max-width 1300) ; 限制图片最大宽度,单位像素
  (setq dashboard-image-banner-max-height 800) ; 限制图片最大高度,单位像素
  (setq dashboard-startup-banner "/home/xtt/.emacs.d/elpa/dashboard-20210928.656/banners/EMACS.png") ; 这里可以换成你的图片
  (setq dashboard-items '((recents . 6)))
  )

其中 (setq dashboard-startup-banner ...) ... 处可以填写图片路径、txt 文件路径或者 'logo。它们分别会显示图片、文本、Emacs 的 logo。

重新启动 Emacs 可能会报错找不到“linum-mode”。我们可以在上述配置前加入

(defun linum-mode (&optional arg))

这样定义了一个空的函数,就不会报错了。

效果

主题

Emacs 默认的界面陈旧丑陋,不加载主题实在没法看。

目前网上有很多优秀主题,你可以直接下载,此处以 Dracula Theme 为例,在 Emacs 中执行 M-x package-install RET dracula-theme 即可安装,在 init.el 中写入 (load-theme 'dracula t) 即可启用。

这里再给出一个浏览 Emacs 主题的网站。

蒟蒻用的是自己写的主题。如果你也想用,就新建文件 ~/.emacs.d/init/oi-wiki-dark-theme.el,写入

;;; oi-wiki-dark-theme.el --- OI Wiki Dark C++ Theme -*- lexical-binding: t; -*-

(deftheme oi-wiki-dark "AKIOI")

(let* ((class '((class color) (min-colors 89)))
       ;; Base palette
       (bg         "#272a35")
       (fg         "#b6b8c2")
       (red        "#e6695b")
       (prep       "#f06090")
       (green      "#2fb170")
       (blue       "#6791e0")
       (purple     "#c973d9")
       (grey       "#90929a")
       (lavender   "#9383e2")
       (cursor     "#b6b8c2")
       (divider    "#3d404a"))

  (custom-theme-set-faces 'oi-wiki-dark

   ;; Basics
   `(default                  ((,class (:background ,bg :foreground ,fg))))
   `(cursor                   ((,class (:background ,cursor))))
   `(line-number              ((,class (:foreground ,grey))))
   `(line-number-current-line ((,class (:foreground ,fg))))
   `(show-paren-match         ((,class (:foreground unspecified :weight bold))))
   `(region                   ((,class (:foreground unspecified :background "#68406d"))))
   `(fringe                   ((,class (:background nil))))
   `(window-divider           ((,class (:foreground ,divider))))
   `(minibuffer-prompt        ((,class (:foreground ,blue))))
   `(link                     ((,class (:foreground "#526cfe" :underline t))))
   `(error                    ((,class (:foreground "#ff1700" :weight bold))))

   `(company-tooltip                  ((,class (:foreground ,fg :background "#1e2129"))))
   `(company-tooltip-annotation       ((,class (:foreground ,grey))))
   `(company-tooltip-common           ((,class (:foreground "#00bda4"))))
   `(company-tooltip-selection        ((,class (:foreground ,fg :background "#212b3e" :weight bold))))
   `(company-tooltip-common-selection ((,class (:foreground "#00bda4" :background "#212b3e" :weight bold))))
   `(company-scrollbar-bg             ((,class (:foreground nil :background nil))))
   `(company-scrollbar-fg             ((,class (:foreground nil :background nil))))
   `(company-preview                  ((,class (:foreground ,grey))))
   `(company-preview-common           ((,class (:foreground ,grey :weight normal))))

   `(mode-line                          ((t (:foreground ,fg :background "#1e2129" :box nil))))
   `(mode-line-inactive                 ((t (:foreground ,grey :background "#2e303e" :box nil))))
   `(xtt-modeline-buffer-state-modified ((,class (:foreground "#ff1700"))))
   `(xtt-modeline-buffer-state-saved    ((,class (:foreground ,green))))
   `(xtt-modeline-file-size             ((,class (:foreground ,lavender))))
   `(xtt-modeline-buffer-name           ((,class (:foreground ,blue :weight bold))))
   `(xtt-modeline-major-mode            ((,class (:foreground ,purple))))
   `(xtt-modeline-separator             ((,class (:foreground ,grey))))
   `(xtt-modeline-coding-info           ((,class (:foreground ,green))))
   `(xtt-modeline-position              ((,class (:foreground ,fg))))
   `(xtt-modeline-percentage            ((,class (:foreground ,lavender))))

   ;; Syntax faces (font-lock)
   `(font-lock-keyword-face        ((,class (:foreground ,blue))))
   `(font-lock-type-face           ((,class (:foreground ,blue))))
   `(font-lock-string-face         ((,class (:foreground ,green))))
   `(font-lock-variable-name-face  ((,class (:foreground ,fg))))
   `(font-lock-function-name-face  ((,class (:foreground ,purple))))
   `(font-lock-function-call-face  ((,class (:foreground ,fg))))
   `(font-lock-comment-face        ((,class (:foreground ,grey))))
   `(font-lock-preprocessor-face   ((,class (:foreground ,prep))))
   `(font-lock-constant-face       ((,class (:foreground ,lavender))))
   `(font-lock-escape-face         ((,class (:foreground ,red))))
   `(font-lock-number-face         ((,class (:foreground ,red))))
   `(font-lock-delimiter-face      ((,class (:foreground ,grey))))
   `(font-lock-punctuation-face    ((,class (:foreground ,grey))))
   `(font-lock-operator-face       ((,class (:foreground ,grey))))
   `(font-lock-bracket-face        ((,class (:foreground ,grey))))
   `(font-lock-builtin-face        ((,class (:foreground ,blue))))

   `(consult-highlight-match ((,class (:foreground "#00bda4"))))

   `(vertico-current ((,class (:background "#32353c" :weight bold))))

   `(orderless-match-face-0 ((,class (:foreground "#00bda4"))))
   `(orderless-match-face-1 ((,class (:foreground "#00bda4"))))
   `(orderless-match-face-2 ((,class (:foreground "#00bda4"))))
   `(orderless-match-face-3 ((,class (:foreground "#00bda4"))))

   `(completions-common-part  ((,class (:foreground "#00bda4"))))

   `(isearch ((,class (:foreground ,bg :background ,blue))))
   )

  (custom-theme-set-variables
   'oi-wiki-dark
   `(window-divider-default-places 'right-only)
   `(window-divider-default-right-width 2)
   ))

(window-divider-mode 1)

(define-minor-mode oi-wiki-cpp-numbers-mode
  "在C++模式下高亮数字"
  :lighter ""
  (if oi-wiki-cpp-numbers-mode
      (progn
        (font-lock-add-keywords
         nil
         '(("\\<[-+]?\\(?:[0-9]*\\.\\)?[0-9]+\\(?:[eE][-+]?[0-9]+\\)?[fFlL]?\\b"
            (0 (unless (or (nth 4 (syntax-ppss))
                           (nth 3 (syntax-ppss)))
                 'font-lock-number-face)))
           ("\\<[-+]?[0-9]+\\(?:[uUlL]+\\)?\\b"
            (0 (unless (or (nth 4 (syntax-ppss))
                           (nth 3 (syntax-ppss)))
                 'font-lock-number-face)))
           ("\\<[-+]?0[xX][0-9a-fA-F]+[uUlL]*\\b"
            (0 (unless (or (nth 4 (syntax-ppss))
                           (nth 3 (syntax-ppss)))
                 'font-lock-number-face)))
           ("\\<[-+]?0[bB][01]+[uUlL]*\\b"
            (0 (unless (or (nth 4 (syntax-ppss))
                           (nth 3 (syntax-ppss)))
                 'font-lock-number-face)))
           ("\\<[-+]?0[0-7]+[uUlL]*\\b"
            (0 (unless (or (nth 4 (syntax-ppss))
                           (nth 3 (syntax-ppss)))
                 'font-lock-number-face)))))
        (font-lock-flush))
    (font-lock-remove-keywords
     nil
     '((nil (0 nil))))
    (font-lock-flush)))

(define-minor-mode oi-wiki-cpp-symbols-mode
  "在C++模式下高亮符号为灰色"
  :lighter ""
  (if oi-wiki-cpp-symbols-mode
      (progn
        (font-lock-remove-keywords nil '((nil (0 nil))))
        (font-lock-add-keywords
         nil
         '(("[][{}()]" 
            (0 (unless (or (nth 4 (syntax-ppss))
                           (nth 3 (syntax-ppss)))
                 'font-lock-bracket-face)))))
        (font-lock-add-keywords
         nil
         '(
           ("[,;:]" 
            (0 (unless (or (nth 4 (syntax-ppss))
                           (nth 3 (syntax-ppss)))
                 'font-lock-punctuation-face)))
           ("[+*/%=&|^!~<>?.?-]"
            (0 (unless (or (nth 4 (syntax-ppss))
                           (nth 3 (syntax-ppss)))
                 'font-lock-operator-face)))
           ("\\(?:->\\|\\+\\+\\|--\\|<<\\|>>\\|&&\\|||\\|::\\|\\+=\\|-=\\|\\*=\\|/=\\|%=\\|&=\\||=\\|^=\\|<<=\\|>>=\\|<=\\|>=\\|==\\|!=\\|->\\*\\|\\.\\*\\)"
            (0 (unless (or (nth 4 (syntax-ppss))
                           (nth 3 (syntax-ppss)))
                 'font-lock-operator-face)))))
        (font-lock-flush))
    (font-lock-remove-keywords
     nil
     '((nil (0 nil))))
    (font-lock-flush)))

(defun oi-wiki-dark-cpp-hook ()
  "在C++模式下启用数字和符号高亮"
  (oi-wiki-cpp-symbols-mode 1)
  (oi-wiki-cpp-numbers-mode 1)
  (when (buffer-file-name)
    (font-lock-flush)))

(add-hook 'c++-mode-hook 'oi-wiki-dark-cpp-hook)

(dolist (buffer (buffer-list))
  (with-current-buffer buffer
    (when (derived-mode-p 'c++-mode)
      (oi-wiki-cpp-numbers-mode 1)
      (oi-wiki-cpp-symbols-mode 1))))

(provide-theme 'oi-wiki-dark)

;;; oi-wiki-dark-theme.el ends here

你也可以在此基础上按意愿进行魔改。

接着,在 init.el 中写上

(add-to-list 'custom-theme-load-path "~/.emacs.d/init/")
(load-theme 'oi-wiki-dark t)

然后就可以体验主题了。

(后话:本来我想用 Emacs29 内置的 treesit 来增强语法高亮,但是这玩意自带像答辩一样的缩进,怎么都搞不好,索性就不用了。不过听说 treesit 在新版中有修复,Emacs30+ 的读者可以尝试一下)

自定义 modeline

网上已经有很多优秀的 modeline 了,比如 doom modeline,spaceline,powerline, smart-mode-line 等等,大家可以多多尝试。

这里蒟蒻自己写了一个 modeline。

新建文件 ~/.emacs.d/init/modeline.el 并写入:

(defface xtt-modeline-buffer-state
  '((t :weight bold))
  "Face for buffer state indicator (* for modified, - for saved).")

(defface xtt-modeline-buffer-state-modified
  '((t :inherit xtt-modeline-buffer-state
       :foreground "#ff6c6b"))
  "Face for modified buffer state.")

(defface xtt-modeline-buffer-state-saved
  '((t :inherit xtt-modeline-buffer-state
       :foreground "#51afef"))
  "Face for saved buffer state.")

(defface xtt-modeline-file-size
  '((t :foreground "#a9a1e1"))
  "Face for file size indicator.")

(defface xtt-modeline-buffer-name
  '((t :weight bold :foreground "#dcaeea"))
  "Face for buffer name.")

(defface xtt-modeline-major-mode
  '((t :slant italic :foreground "#98be65"))
  "Face for major mode name.")

(defface xtt-modeline-separator
  '((t :foreground "#5B6268"))
  "Face for separators.")

(defface xtt-modeline-coding-info
  '((t :foreground "#46d9ff"))
  "Face for coding system info.")

(defface xtt-modeline-position
  '((t :foreground "#bbc2cf"))
  "Face for cursor position (line,column).")

(defface xtt-modeline-percentage
  '((t :foreground "#da8548"))
  "Face for Top/Bottom/ALL/percentage indicator.")

(defun setup-enhanced-modeline ()
  (interactive)

  (defun xtt-modeline-buffer-state ()
    (if (buffer-modified-p)
        (propertize " *" 'face (xtt-modeline-effective-face 'xtt-modeline-buffer-state-modified))
      (propertize " -" 'face (xtt-modeline-effective-face 'xtt-modeline-buffer-state-saved))))

  (defun xtt-modeline-file-size ()
    (if buffer-file-name
        (let ((size (buffer-size)))
          (propertize
           (cond
            ((> size 1000000) (format "%.1fM" (/ size 1000000.0)))
            ((> size 1000) (format "%.1fK" (/ size 1000.0)))
            (t (format "%d" size)))
           'face (xtt-modeline-effective-face 'xtt-modeline-file-size)))
      ""))

  (defun separator1 ()
    (propertize " > " 'face (xtt-modeline-effective-face 'xtt-modeline-separator)))

  (defun separator2 ()
    (propertize " < " 'face (xtt-modeline-effective-face 'xtt-modeline-separator)))

  (defun xtt-modeline-flymake-info ()
    (let ((s (when (and (bound-and-true-p flymake-mode)
                        (boundp 'flymake-mode-line-format))
               (format-mode-line flymake-mode-line-format))))
      (if (and (stringp s) (not (string-empty-p s))) s "")))

  (defun xtt-modeline-coding-info ()
    (let ((coding buffer-file-coding-system))
      (propertize
       (cond
        ((not coding) "UTF-8")
        ((string-match-p "utf-8" (symbol-name coding)) "UTF-8")
        ((string-match-p "gbk" (symbol-name coding)) "GBK")
        ((string-match-p "big5" (symbol-name coding)) "Big5")
        (t (upcase (substring (symbol-name coding) 0 4))))
       'face (xtt-modeline-effective-face 'xtt-modeline-coding-info))))

  (defun xtt-modeline-percent-indicator ()
    (let* ((start (window-start))
           (end   (window-end nil t))
           (max   (point-max)))
      (cond
       ;; 全部可见
       ((and (<= start 1) (>= end max)) "All")
       ;; 顶部
       ((<= start 1) "Top")
       ;; 底部
       ((>= end max) "Bot")
       ;; 百分比(缓冲区顶部以上的比例
      (t (format "%d%%%%"
                  (floor (* 100.0
                            (/ (float (max 0 (1- start)))
                               (max 1.0 (float max))))))))))

  (defvar xtt-modeline--left
  `(
    (:eval (xtt-modeline-buffer-state))
    (:eval (let ((size (xtt-modeline-file-size)))
             (if (string-empty-p size)
                 ""
               (concat (separator1) size))))
    ,(separator1)
    (:eval (propertize "%b" 'face (xtt-modeline-effective-face 'xtt-modeline-buffer-name)))
    ,(separator1)
    (:eval (propertize (format-mode-line mode-name)
                       'face (xtt-modeline-effective-face 'xtt-modeline-major-mode)))
    (:eval (let* ((info (xtt-modeline-flymake-info))
                  (info (if (and (stringp info)
                                 (> (length info) 0)
                                 (eq (aref info 0) ?\s))
                            (substring info 1)
                          info)))
             (if (and (stringp info) (not (string-empty-p info)))
                 (concat (separator1) info)
               "")))
    )
  "Left part of modeline.")

  (defvar xtt-modeline--right
    `(
      (:eval (xtt-modeline-coding-info))
      ,(separator2)
      (:eval (propertize "(%l,%c)" 'face (xtt-modeline-effective-face 'xtt-modeline-position)))
      ,(separator2)
      (:eval (propertize (xtt-modeline-percent-indicator) 'face (xtt-modeline-effective-face 'xtt-modeline-percentage)))
      )
    "Right part of modeline.")

  (defun xtt-modeline--window-active-p ()
    (mode-line-window-selected-p))

  (defun xtt-modeline-render ()
    (if (xtt-modeline--window-active-p)
      (let* ((left (format-mode-line xtt-modeline--left))
           (right (concat
               (xtt-modeline-coding-info)
               (separator2)
               (propertize (format "(%d,%d)" (line-number-at-pos) (current-column))
                     'face (xtt-modeline-effective-face 'xtt-modeline-position))
               (separator2)
               (propertize (xtt-modeline-percent-indicator)
                     'face (xtt-modeline-effective-face 'xtt-modeline-percentage)))))
        (concat left
            (propertize " " 'display '(space :align-to (- right 26)))
            right))
      (let* ((remaps '((xtt-modeline-buffer-state-modified :inherit xtt-modeline-buffer-state-modified :foreground unspecified)
                       (xtt-modeline-buffer-state-saved    :inherit xtt-modeline-buffer-state-saved    :foreground unspecified)
                       (xtt-modeline-file-size             :inherit xtt-modeline-file-size             :foreground unspecified)
                       (xtt-modeline-buffer-name           :inherit xtt-modeline-buffer-name           :foreground unspecified)
                       (xtt-modeline-major-mode            :inherit xtt-modeline-major-mode            :foreground unspecified)
                       (xtt-modeline-separator             :inherit xtt-modeline-separator             :foreground unspecified)
                       (xtt-modeline-coding-info           :inherit xtt-modeline-coding-info           :foreground unspecified)
                       (xtt-modeline-position              :inherit xtt-modeline-position              :foreground unspecified)
                       (xtt-modeline-percentage            :inherit xtt-modeline-percentage            :foreground unspecified)))
             (face-remapping-alist (append remaps face-remapping-alist))
                 (left (format-mode-line xtt-modeline--left))
                 (right (concat
                   (xtt-modeline-coding-info)
                   (separator2)
                   (propertize (format "(%d,%d)" (line-number-at-pos) (current-column))
                   'face (xtt-modeline-effective-face 'xtt-modeline-position))
                   (separator2)
                   (propertize (xtt-modeline-percent-indicator)
                   'face (xtt-modeline-effective-face 'xtt-modeline-percentage)))))
        (concat (propertize left 'face 'mode-line-inactive)
                (propertize " " 'display '(space :align-to (- right 26)))
                (propertize right 'face 'mode-line-inactive)))))

  (setq-default mode-line-format '((:eval (xtt-modeline-render))))
  )

  (defun xtt-modeline-effective-face (face)
    face)

(setup-enhanced-modeline)

(defun xtt-enforce-inactive-modeline-foreground ()
  "Force `mode-line-inactive' foreground to a consistent gray color." 
  (set-face-attribute 'mode-line-inactive nil :foreground "#5B6268"))

(add-hook 'emacs-startup-hook #'xtt-enforce-inactive-modeline-foreground)

(advice-add 'load-theme :after (lambda (&rest _) (xtt-enforce-inactive-modeline-foreground)))

(add-hook 'after-make-frame-functions
          (lambda (&rest _) (xtt-enforce-inactive-modeline-foreground)))

(when (boundp 'flymake-mode)
  (add-hook 'flymake-mode-hook 
            (lambda () 
              (force-mode-line-update t))))

(provide 'modeline)

然后在 init.el 中写上

(add-to-list 'load-path "~/.emacs.d/init/")
(require 'modeline)

就可以用了。

这个 modeline 自定义了一些 face 供自行修改。

结语

Emacs 不配置简直连记事本都不如,所以考场不建议用 Emacs。

蒟蒻对 Emacs 了解甚微,关于 Emacs 的配置就这么多,但是希望这篇文章可以抛砖引玉,引发更多优秀的 Emacs 配置。

配置虽少,但期间好歹经历了无数次挫折,还因折腾 Emacs 重装了两次系统。现在分享给大家,只求一个小小的赞。

完结撒花*~~

参考资料