Emacs——神的编辑器入门教程
a_cow_of_FJ · · 科技·工程
更好的食用体验
引言
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 初次打开后应该长这样:
下面介绍一些基本概念。
- frame:打开的一整个窗口称作 frame。打开多个窗口就有多个 frame。上图就是一个 frame。
- menu bar:菜单栏,即窗口顶部“File Edit...”那一栏。
- tool bar:工具栏,即 menu bar 下面的栏目。
- buffer:缓冲即打开的文件和进程,在不保存的情况下,在缓冲中修改并不会修改到文件。在缓冲区的底部(modeline 上)点击缓冲的名字或者使用快捷键可以切换缓冲。上图打开了“GNU Emacs”这个 buffer。
- window:tool bar 以下,minibuffer 以上一整个区域称作一个 window,分屏可以有多个 window。
- modeline:位于窗口底部的模式线,用来显示当前 buffer 信息。
- minibuffer:modeline 下面就是 minibuffer。minibuffer 用来显示一些信息或提示。上图中 minibuffer 里显示着“Beggining of buffer”的字样。在 minibuffer 输入时可以向终端那样按
Tab补全。
基本操作
基本快捷键
为方便叙述,以后用 C 代表 Ctrl 键;M 代表 Alt 键;C-? 或 M-? 表示同时按下 C 和 ? 或 M 和 ?;C-M-? 表示同时按下 C,M 和 ?;? ? 表示依次按下前后两个键。
打开一个目录: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 根据文件内容搜索文件。它们分别有两种选择:
根据名称搜索:
consult-findconsult-fd(windows 用不了)
根据内容搜索:
consult-grepconsult-ripgrep
编号为 1 的两个工具可以直接使用,而编号为 2 的两个工具需要手动安装,不过后者速度几乎碾压前者,因此我们选择后者。
安装 rg
我们前往 ripgrep 官网,在 release 中找到你需要的压缩包下载,然后把解压出来的 rg 文件放在以下目录:
- linux:
/usr/local/bin/ - windows:
C:\Windows\System32\
回到 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 重装了两次系统。现在分享给大家,只求一个小小的赞。
完结撒花*~~
参考资料
-
专业 Emacs 入门教程
-
Emacs 官方文档
-
21 天学会 Emacs
-
Emacs - OI wiki
-
Emacs 中文论坛