Emacs 是 Hacker 的一种生活方式,而这是符合我口味的私人定制。
更多 Emacs 内容请访问我的博客。
从源码编译、安装最新 Emacs 30 版本:
brew uninstall emacs-plus@30
brew install emacs-plus@30 --with-xwidgets --with-imagemagick --with-poll \
--with-dragon-icon --with-native-comp --with-poll
brew unlink emacs-plus@30 && brew link emacs-plus@30
ln -sf /opt/homebrew/opt/emacs-plus@30/Emacs.app /Applications/
安装 native 编译所需的 gcc 工具链:
brew install gcc
current_gcc="/opt/homebrew/opt/gcc/lib/gcc/current"
export LIBRARY_PATH="${LIBRARY_PATH}:${current_gcc}:${current_gcc}/gcc/aarch64-apple-darwin23/14/"
early-init.el
是 Emacs 启动早期执行的第一个初始化文件:
;; 个人信息。
(setq user-full-name "zhangjun")
(setq user-mail-address "[email protected]")
;; native 编译。
(when (fboundp 'native-compile-async)
(setenv "LIBRARY_PATH"
(concat (getenv "LIBRARY_PATH")
":/opt/homebrew/opt/gcc/lib/gcc/current/"
":/opt/homebrew/opt/gcc/lib/gcc/current/gcc/aarch64-apple-darwin23/14/"))
(setq native-comp-speed 4)
(setq native-comp-async-jobs-number 8)
;;(setq inhibit-automatic-native-compilation t)
(setq native-comp-async-report-warnings-errors 'silent))
;; 解决 cmake 和 Xcode 15.0 兼容性问题,否则后续编译 vterm-module 时报错。
;;; 查看当前 Xcode SDK 路径:xcrun --sdk macosx --show-sdk-path
(setenv "SDKROOT" "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk")
;; 加载较新的 .el 文件。
(setq-default load-prefer-newer t)
(setq-default lexical-binding t)
(setq lexical-binding t)
;; 在独立文件保存 Emacs 自动写入的配置参数,避免污染 ~/.emacs 文件。
(setq custom-file (expand-file-name "~/.emacs.d/custom.el"))
(add-hook 'after-init-hook
(lambda ()
(when (file-exists-p custom-file)
(load custom-file))))
;; 非系统标准路径的二进制目录列表。
(setq my-bin-path
'("/opt/homebrew/bin"
"/opt/homebrew/opt/findutils/libexec/gnubin"
"/opt/homebrew/opt/openjdk/bin"
"/Users/alizj/go/bin"
"/Users/alizj/.cargo/bin"))
;; 添加到 PATH 环境变量。
(mapc (lambda (p)
(setenv "PATH" (concat p ":" (getenv "PATH"))))
my-bin-path)
;; 添加到 Emacs 搜索路径:Emacs 查找外部程序时使用 `exec-path` 而非 `PATH` 变量。
(let ((paths my-bin-path))
(dolist (path paths)
(setq exec-path (cons path exec-path))))
设置软件包源:
M-x use-package-report
: 查看包加载时间(按 S 排序);M-x package-vc-upgrade-all
:更新使用:vc
指令配置的,从github
安装的包;
(require 'package)
(setq package-archives
'(("elpa" . "http://mirrors.tuna.tsinghua.edu.cn/elpa/gnu/")
("elpa-devel" . "http://mirrors.tuna.tsinghua.edu.cn/elpa/gnu-devel/")
("melpa" . "http://mirrors.tuna.tsinghua.edu.cn/elpa/melpa/")
("nongnu" . "http://mirrors.tuna.tsinghua.edu.cn/elpa/nongnu/")
("nongnu-devel" . "http://mirrors.tuna.tsinghua.edu.cn/elpa/nongnu-devel/")))
(package-initialize)
(when (not package-archive-contents)
(package-refresh-contents))
(setq use-package-verbose t)
(setq use-package-always-ensure t)
(setq use-package-always-demand t)
(setq use-package-compute-statistics t)
(setq use-package-vc-prefer-newest t)
;; 允许升级 Emacs 内置的包。
;;(setq package-install-upgrade-built-in t)
设置 GPG 加解密:
(setq auth-sources '("~/.authinfo.gpg"))
;;(setq auth-source-debug t)
(use-package epa
:config
(setq-default
;; 缺省使用 email 地址加密。
epa-file-encrypt-to user-mail-address
;; 使用 minibuffer 输入 GPG 密码。
epa-pinentry-mode 'loopback)
(require 'epa-file)
(epa-file-enable))
按键调整:
;; command 作为 Meta 键。
(setq mac-command-modifier 'meta)
;; option 作为 Super 键。
(setq mac-option-modifier 'super)
;; fn 作为 Hyper 键。
(setq ns-function-modifier 'hyper)
;; 关闭容易误操作的按键。
;; s- 表示 Super,S- 表示 Shift, H- 表示 Hyper:
(let ((keys '(
"s-w"
"C-z"
"<mouse-2>"
"s-k"
"s-,"
"s-."
"s--"
"s-+"
"C-<wheel-down>"
"C-<wheel-up>"
"C-M-<wheel-down>"
"C-M-<wheel-up>"
;;"<down-mouse-1>"
;;"<drag-mouse-1>"
)))
(dolist (key keys)
(global-unset-key (kbd key))))
提升 IO 性能,参考 doom core.el :
(setq process-adaptive-read-buffering nil)
(setq read-process-output-max (* 1024 1024 4))
(setq inhibit-compacting-font-caches t)
(setq-default message-log-max t)
;; Garbage Collector Magic Hack, 提升 GC 性能。
(use-package gcmh
:init
;;(setq gcmh-verbose t)
(setq gcmh-idle-delay 'auto) ;; 缺省 15s
(setq gcmh-auto-idle-delay-factor 10)
(setq gcmh-high-cons-threshold (* 32 1024 1024))
(gcmh-mode 1)
(gcmh-set-high-threshold))
;;(setq garbage-collection-messages t)
(add-hook 'after-init-hook #'garbage-collect t)
MacOS
自带的 curl
不支持 socks5
代理, 这里安装支持 socks5
的 GNU curl
版本:
brew install curl
export PATH="/opt/homebrew/opt/curl/bin:$PATH"
将 GNU curl
添加到 Emacs
的 PATH
环境变量和 exec-path
变量中:
(setq my-coreutils-path "/opt/homebrew/opt/curl/bin/")
(setenv "PATH" (concat my-coreutils-path ":" (getenv "PATH")))
(setq exec-path (cons my-coreutils-path exec-path))
Emacs
使用 url-retrieve
访问 URL,这里设置它使用 curl
后端,这样全局可使用 socks5 代理:
;; socks5 代理信息。
(setq my/socks-host "127.0.0.1")
(setq my/socks-port 1080)
(setq my/socks-proxy (format "socks5h://%s:%d" my/socks-host my/socks-port))
;; 不经过 socks 代理的 CIDR 或域名列表, 需要同时满足 socks-noproxy 和 NO_RROXY 值要求:
;; + socks-noproxy: 域名是正则表达式, 如 \\.baidu.com;
;; + NO_PROXY: 域名支持 *.baidu.com 或 baidu.com;
;; 所以这里使用的是同时满足两者的域名后缀形式, 如 .baidu.com;
(setq my/no-proxy
'(
"127.0.0.1/32"
"10.0.0.0/8"
"172.0.0.0/8"
"0.0.0.0/32"
"localhost"
"192.168.0.0/16"
".cn"
".alibaba-inc.com"
".taobao.com"
".antfin-inc.com"
".openai.azure.com"
".baidu.com"
".aliyun-inc.com"
".aliyun-inc.test"
))
(setq my/user-agent
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36")
(use-package mb-url-http
:demand
:vc (:url "https://github.com/dochang/mb-url")
:init
(require 'auth-source)
(let ((credential (auth-source-user-and-password "api.github.com")))
(setq github-user (car credential)
github-password (cadr credential))
(setq github-auth (concat github-user ":" github-password))
(setq mb-url-http-backend 'mb-url-http-curl
mb-url-http-curl-program "/opt/homebrew/opt/curl/bin/curl"
mb-url-http-curl-switches
`("-k"
"-x" ,my/socks-proxy
"--keepalive-time" "60"
"--keepalive"
"--max-time" "300"
;;防止 POST 超过 1024 Bytes 时发送 `Expect: 100-continue` 导致 1s 延迟。
"-H" "Expect: ''"
;;"-u" ,github-auth
"--user-agent" ,my/user-agent
))))
;; 开启 socks5 代理。
(defun proxy-socks-enable ()
(interactive)
(require 'socks)
(setq url-gateway-method 'socks
socks-noproxy my/no-proxy
socks-server `("Default server" ,my/socks-host ,my/socks-port 5))
(let ((no-proxy (mapconcat 'identity my/no-proxy ",")))
(setenv "no_proxy" no-proxy))
(setenv "ALL_PROXY" my/socks-proxy)
(setenv "ALL_PROXY" my/socks-proxy)
(setenv "HTTP_PROXY" nil)
(setenv "HTTPS_PROXY" nil)
(advice-add 'url-http :around 'mb-url-http-around-advice))
;; 关闭 socks5 代理。
(defun proxy-socks-disable ()
(interactive)
(require 'socks)
(setq url-gateway-method 'native socks-noproxy nil)
(setenv "all_proxy" "")
(setenv "ALL_PROXY" ""))
;; 默认启动时开启 socks5 代理。
(proxy-socks-enable)
关闭部分 UI 元素:
(when (memq window-system '(mac ns x))
(tool-bar-mode -1)
(scroll-bar-mode -1)
(menu-bar-mode -1)
(setq use-file-dialog nil)
(setq use-dialog-box nil))
光标和行号:
;; 高亮当前行。
(global-hl-line-mode t)
(setq global-hl-line-sticky-flag t)
;; 显示行号。
(global-display-line-numbers-mode t)
;; 设置光标样式。
(setq-default cursor-type 'bar)
;; 光标和字符宽度一致(如 TAB)。
(setq x-stretch-cursor t)
Frame 设置:
;; frame 边角样式:undecorated, round corner: undecorated-round
(add-to-list 'default-frame-alist '(undecorated . t))
(add-to-list 'default-frame-alist '(ns-transparent-titlebar . t))
(add-to-list 'default-frame-alist '(selected-frame) 'name nil)
(add-to-list 'default-frame-alist '(ns-appearance . dark))
;; 新建 frame window 的大小。
(add-to-list 'default-frame-alist '(height . 24))
(add-to-list 'default-frame-alist '(width . 80))
;; 不在新 frame 打开文件(如 Finder 的 "Open with Emacs") 。
(setq ns-pop-up-frames nil)
;; 复用当前 frame。
(setq display-buffer-reuse-frames t)
(setq frame-resize-pixelwise t)
;; 30: 左右分屏, nil: 上下分屏。
(setq split-width-threshold nil)
;; 刷新显示。
(global-set-key (kbd "<f5>") #'redraw-display)
(setq switch-to-buffer-obey-display-actions t)
;; 在 frame 底部显示的窗口列表。
(add-to-list
'display-buffer-alist
`((,(regexp-opt
'("\\*compilation\\*"
"\\*Apropos\\*"
"\\*Help\\*"
"\\*helpful"
"\\*info\\*"
"\\*Summary\\*"
"\\*vt"
"\\*lsp-bridge"
"\\*Org"
"\\*Google Translate\\*"
" \\*eglot"
"Shell Command Output"))
;; 复用同名 buffer 窗口。
(display-buffer-reuse-window
. (
;; 在 frame 底部显示窗口。
(side . bottom)
;; 窗口高度比例。
(window-height . 0.35)
)))))
;; 启动后显示模式,加 t 参数让 togg-frame-XX 最后运行,这样才生效:
(add-hook 'window-setup-hook 'toggle-frame-maximized t) ;; toggle-frame-fullscreen
窗口和滚动:
;; 切换窗口。
(global-set-key (kbd "s-o") #'other-window)
(setq window-combination-resize t)
;; 像素平滑滚动。
(pixel-scroll-precision-mode t)
(setq fast-but-imprecise-scrolling t)
(setq scroll-conservatively 10
scroll-margin 2
scroll-preserve-screen-position t
mouse-wheel-scroll-amount '(2 ((shift) . hscroll))
mouse-wheel-scroll-amount-horizontal 2)
dashboard:
(use-package dashboard
:config
(dashboard-setup-startup-hook)
(setq-local global-hl-line-mode nil)
(setq dashboard-banner-logo-title "Happy Hacking & Writing 🎯")
(setq dashboard-projects-backend #'project-el)
(setq dashboard-center-content t)
(setq dashboard-set-heading-icons t)
(setq dashboard-set-navigator t)
(setq dashboard-set-file-icons t)
(setq dashboard-path-max-length 30)
;; 显示 org-mode agenda。
(add-to-list 'dashboard-items '(agenda) t)
(setq dashboard-items '((recents . 20) (projects . 8) (agenda . 3))))
doom-modeline:
;; 使用 Symbols Nerd Fonts Mono 在 modeline 上显示 icons,需要单独下载和安装该字体。
(use-package nerd-icons)
(use-package doom-modeline
:hook (after-init . doom-modeline-mode)
:custom
(doom-modeline-buffer-encoding nil)
(doom-modeline-env-version nil)
(doom-modeline-env-enable-rust nil)
(doom-modeline-env-enable-go nil)
(doom-modeline-buffer-file-name-style 'truncate-nil)
(doom-modeline-vcs-max-length 30)
(doom-modeline-github nil)
(doom-modeline-time-icon nil)
(doom-modeline-check-simple-format t)
:config
(display-battery-mode 0)
(column-number-mode t)
(display-time-mode t)
(setq display-time-24hr-format t)
(setq display-time-default-load-average nil)
(setq display-time-load-average-threshold 20)
(setq display-time-format "%H:%M ") ;; 默认:"%m/%d[%w]%H:%M "
(setq indicate-buffer-boundaries (quote left)))
;; 为 vterm-mode 定义简化的 modeline,避免 vterm buffer 内容过多时更新 modeline 影响性能。
(doom-modeline-def-modeline 'my-vterm-modeline
'(buffer-info) ;; 左侧
'(misc-info minor-modes input-method)) ;; 右侧
(add-to-list 'doom-modeline-mode-alist '(vterm-mode . my-vterm-modeline))
dired-sidebar:使用 dired 显示目录,相比 treemacs/neotree 的优势是速度快和使用 dired 按键:
(use-package vscode-icon
:commands (vscode-icon-for-file))
(use-package dired-sidebar
:bind (("s-1" . dired-sidebar-toggle-sidebar))
:commands (dired-sidebar-toggle-sidebar)
:init
(add-hook 'dired-sidebar-mode-hook
(lambda ()
(unless (file-remote-p default-directory)
(auto-revert-mode))))
:config
(push 'toggle-window-split dired-sidebar-toggle-hidden-commands)
(push 'rotate-windows dired-sidebar-toggle-hidden-commands)
(setq dired-sidebar-subtree-line-prefix "-")
(setq dired-sidebar-theme 'vscode) ;;'ascii
(setq dired-sidebar-use-term-integration t)
(setq dired-sidebar-use-one-instance t)
(setq dired-sidebar-use-custom-font t)
(setq dired-sidebar-icon-scale 0.1)
(setq dired-sidebar-follow-file-idle-delay 0.5))
字体:英文字体 Iosevka/Sarasa 和中文字体 LxgwWenKai,按照 1:1 缩放,在偶数字号的情况 下可以实现中英文等宽等高。
- 英文:Iosevka Comfy;
- 中文:霞鹜文楷屏幕阅读版 LxgwWenKai-Screen,对字体做了加粗,便于屏幕阅读;
常用字体命令:
- 查看 Emacs 支持的字体名称:
(print (font-family-list))
- 查看光标处字体:
M-x describe-char
- 查看 Emacs 支持的字体名称:
(print (font-family-list))
(use-package fontaine
:config
(setq fontaine-latest-state-file (locate-user-emacs-file "fontaine-latest-state.eld"))
(setq fontaine-presets
'((regular) ;; 使用缺省配置。
(t
:default-family "Iosevka Comfy"
:default-weight regular
:default-height 180 ;; 默认字号, 需要是偶数才能实现中英文等宽等高。
:fixed-pitch-family "Iosevka Comfy"
:fixed-pitch-weight nil
:fixed-pitch-height 1.0
:fixed-pitch-serif-family "Iosevka Comfy"
:fixed-pitch-serif-weight nil
:fixed-pitch-serif-height 1.0
:variable-pitch-family "Iosevka Comfy Duo"
:variable-pitch-weight nil
:variable-pitch-height 1.0
:line-spacing nil)))
(fontaine-mode 1)
(add-hook 'enable-theme-functions #'fontaine-apply-current-preset)
(fontaine-set-preset (or (fontaine-restore-latest-preset) 'regular))
(add-hook 'kill-emacs-hook #'fontaine-store-latest-preset))
;; 设置 emoji/symbol 和中文字体。
(defun my/set-font ()
(when window-system
(setq use-default-font-for-symbols nil)
(set-fontset-font t 'emoji (font-spec :family "Apple Color Emoji")) ;; Noto Color Emoji
(set-fontset-font t 'symbol (font-spec :family "Symbola")) ;; Apple Symbols, Symbola
(let ((font (frame-parameter nil 'font))
(font-spec (font-spec :family "LXGW WenKai Screen")))
(dolist (charset '(kana han hangul cjk-misc bopomofo))
(set-fontset-font font charset font-spec)))))
;; Emacs 启动后或 fontaine preset 切换时设置字体。
(add-hook 'after-init-hook 'my/set-font)
(add-hook 'fontaine-set-preset-hook 'my/set-font)
;; 设置字体缩放比例,设置为 1.172 可以确保 2 倍放大后对应的是 22 号偶数字体,这样表格
;; 可以对齐。16 * 1.172 * 1.172 = 21.97(Emacs 取整为 22)。
(setq text-scale-mode-step 1.172)
;; org-table 只使用中英文严格等宽的 LXGW WenKai Mono Screen 字体, 避免中英文不对齐。
(custom-theme-set-faces 'user '(org-table ((t (:family "LXGW WenKai Mono Screen")))))
ef-themes: Emacs 主题列表:https://emacsthemes.com/popular/index.html
(use-package ef-themes
:demand
:config
(mapc #'disable-theme custom-enabled-themes)
(setq ef-themes-variable-pitch-ui t)
(setq ef-themes-mixed-fonts t)
(setq ef-themes-headings
'(
;; level 0 是文档 title,1-8 是文档 header。
(0 . (variable-pitch light 1.9))
(1 . (variable-pitch light 1.8))
(2 . (variable-pitch regular 1.7))
(3 . (variable-pitch regular 1.6))
(4 . (variable-pitch regular 1.5))
(5 . (variable-pitch 1.4))
(6 . (variable-pitch 1.3))
(7 . (variable-pitch 1.2))
(8 . (variable-pitch 1.1))
(t . (variable-pitch 1.1))))
(setq ef-themes-region '(intense no-extend neutral)))
自动切换深浅主题:
(defun my/load-theme (appearance)
(interactive)
(pcase appearance
('light (load-theme 'ef-light t))
('dark (load-theme 'ef-elea-dark t))))
(add-hook 'ns-system-appearance-change-functions 'my/load-theme)
(add-hook 'after-init-hook (lambda () (my/load-theme ns-system-appearance)))
tab-bar:
(use-package tab-bar
:custom
(tab-bar-close-button-show nil)
(tab-bar-new-button-show nil)
(tab-bar-history-limit 20)
(tab-bar-new-tab-choice "*dashboard*")
(tab-bar-show 1)
;; 使用 super + N 切换 tab。
(tab-bar-select-tab-modifiers "super")
:config
;; 去掉最左侧的 < 和 > 。
(setq tab-bar-format '(tab-bar-format-tabs tab-bar-separator))
;; 开启 tar-bar history mode 后才支持 history-back/forward 命令。
(tab-bar-history-mode t)
(global-set-key (kbd "s-f") 'tab-bar-history-forward)
(global-set-key (kbd "s-b") 'tab-bar-history-back)
(global-set-key (kbd "s-t") 'tab-bar-new-tab)
(keymap-global-set "s-n" 'tab-bar-switch-to-next-tab)
(keymap-global-set "s-p" 'tab-bar-switch-to-prev-tab)
(keymap-global-set "s-w" 'tab-bar-close-tab)
;; 为 tab 添加序号,用于快速切换。
(defvar ct/circle-numbers-alist
'((0 . "⓪")
(1 . "①")
(2 . "②")
(3 . "③")
(4 . "④")
(5 . "⑤")
(6 . "⑥")
(7 . "⑦")
(8 . "⑧")
(9 . "⑨"))
"Alist of integers to strings of circled unicode numbers.")
(setq tab-bar-tab-hints t)
(defun ct/tab-bar-tab-name-format-default (tab i)
(let ((current-p (eq (car tab) 'current-tab))
(tab-num (if (and tab-bar-tab-hints (< i 10))
(alist-get i ct/circle-numbers-alist) "")))
(propertize
(concat tab-num
" "
(alist-get 'name tab)
(or (and tab-bar-close-button-show
(not (eq tab-bar-close-button-show
(if current-p 'non-selected 'selected)))
tab-bar-close-button)
"")
" ")
'face (funcall tab-bar-tab-face-function tab))))
(setq tab-bar-tab-name-format-function #'ct/tab-bar-tab-name-format-default)
(global-set-key (kbd "s-1") 'tab-bar-select-tab)
(global-set-key (kbd "s-2") 'tab-bar-select-tab)
(global-set-key (kbd "s-3") 'tab-bar-select-tab)
(global-set-key (kbd "s-4") 'tab-bar-select-tab)
(global-set-key (kbd "s-5") 'tab-bar-select-tab)
(global-set-key (kbd "s-6") 'tab-bar-select-tab)
(global-set-key (kbd "s-7") 'tab-bar-select-tab)
(global-set-key (kbd "s-8") 'tab-bar-select-tab)
(global-set-key (kbd "s-9") 'tab-bar-select-tab))
使用 RIME
输入法 + iDevel/rime-ice
雾凇拼音输入法方案。
安装 RIME
输入法后端引擎 librime : emacs-rime
直接和该引擎打交道,不需要安装前端程序
“鼠须管squirrel”。
wget https://github.com/rime/librime/releases/download/1.11.2/rime-5b09f35-macOS-universal.tar.bz2
tar -xvf rime-5b09f35-macOS-universal.tar.bz2
mv ~/.emacs.d/librime/dist{,.bak}
mv dist ~/.emacs.d/librime
# 如果 MacOS Gatekeeper 阻止第三方软件运行,可以暂时关闭它:
sudo spctl --master-disable
# 后续再开启:sudo spctl --master-enable
下载 iDvel/rime-ice 雾凇拼音输入法方案:
mv ~/Library/Rime ~/Library/Rime.bak
git clone https://github.com/iDvel/rime-ice --depth=1
mv rime-ice ~/Library/Rime
# 后续可以 git pull 更新 rime-ice。
cd ~/Library/Rime
# 自定义词频文件
cp custom_phrase.txt opsnull_custom_phrase.txt
# 修改其中的 db_name
sed -i -e 's/custom_phrase.txt/opsnull_custom_phrase/g' opsnull_custom_phrase.txt
rime_ice 拼音方案调整(如模糊音,动态词频,自定义词语文件等):
- 自定义短语:向自定义短语词典文件
opsnull_custom_phrase.txt
添加自定义短语, custom_prase/db_class 为 stabledb,是只读的,不会动态调频。(可以设置为 tabledb 来 动态调频)。 - 首次添加该文件后需要执行
M-x rime-deploy
和M-x rime-sync
生效。
patch:
switches:
- name: ascii_mode
states: [ 中, A ]
- name: ascii_punct # 中英标点
states: [ ¥, $ ]
# 下面这些开关一般用不到, 故关闭(如候选词中不再显示 emoji).
# - name: traditionalization
# states: [ 简, 繁 ]
# reset: 0
# - name: emoji
# states: [ 💀, 😄 ]
# reset: 1
# - name: full_shape
# states: [ 半角, 全角 ]
# reset: 0
# - name: search_single_char # search.lua 的功能开关,辅码查词时是否单字优先
# abbrev: [词, 单]
# states: [正常, 单字]
# reset: 0
translator/spelling_hints: 0 # 不显示候选词的拼音。
translator/always_show_comments: false #不显示候选者的拼音。
translator/enable_user_dict: true # 根据上屏自动调整词频, 否则根据 *.dict.yaml 中的静态定义的词频率。
custom_phrase/user_dict: "opsnull_custom_phrase" # 自定义短语词典文件,权重最高。
speller/algebra:
# 模糊拼音
# 声母
- derive/^([zcs])h/$1/ # z c s → zh ch sh
- derive/^([zcs])([^h])/$1h$2/ # zh ch sh → z c s
#- derive/^l/n/ # n → l
#- derive/^n/l/ # l → n
# 韵母
- derive/eng$/en/
- derive/en$/eng/
- derive/in/ing/
- derive/ing/in/
# 自动纠错(后者用前者替换)
# ai
- derive/^([wghk])ai$/$1ia/ # wia → wai
# ei
- derive/([wfghkz])ei$/$1ie/ # wie → wei
# ie
- derive/([jqx])ie$/$1ei/ # jei → jie
Rime 输入法全局配置,详细参考 iDvel/rime-ice
patch:
schema_list:
- schema: rime_ice # 只启用 rime_ice 雾凇拼音输入法方案。
menu/page_size: 9 # 显示 9 个候选词。
# 方案选单切换
switcher/hotkeys:
- F4
- "Control+plus" # 按 C-Shit-+ 调出方案选单。
switcher/fold_options: false # 呼出时不折叠。
switcher/abbreviate_options: false # 折叠时不缩写选项
ascii_composer: # 中英文切换
switch_key: # 关闭左边 Shift 中西文切换,而是使用右侧 Shift(避免频繁误按)。
Shift_L: noop
Shift_R: commit_code
key_binder/bindings:
- { when: has_menu, accept: equal, send: Page_Down } # 下一页
- { when: paging, accept: minus, send: Page_Up } # 上一页
- { when: always, accept: "Control+period", toggle: ascii_mode} # 中英文切换
- { when: always, accept: "Control+comma", toggle: ascii_punct} # 中英文标点切换
#- { when: always, accept: "Control+comma", toggle: full_shape} # 全角/半角切换
# 开启 emacs 绑定惯例,这样可以使用 C-x 来修正拼音。需要将这些按键加到
# rime-translate-keybindings变量里后才会生效。 composing 指的是出现候选词列表的时机。
- { When: composing, accept: Control+p, send: Up }
- { when: composing, accept: Control+n, send: Down }
- { when: composing, accept: Control+b, send: Left }
- { when: composing, accept: Control+f, send: Right }
- { when: composing, accept: Control+a, send: Home }
- { when: composing, accept: Control+e, send: End }
- { when: composing, accept: Control+d, send: Delete }
# 从用户数据库中删除误上屏的词语
- { when: composing, accept: Control+k, send: Shift+Delete }
- { when: composing, accept: Control+h, send: BackSpace }
- { when: composing, accept: Control+g, send: Escape }
- { when: composing, accept: Control+bracketleft, send: Escape }
- { when: composing, accept: Control+y, send: Page_Up }
- { when: composing, accept: Alt+v, send: Page_Up }
- { when: composing, accept: Control+v, send: Page_Down }
# 更多按键名称参考: https://github.com/LEOYoon-Tsaw/Rime_collections/blob/master/Rime_description.md
配置 Emacs:
rime-disable-predicates
定义了一组断言函数,当任一函数断言成立时,Rime 自动将输入法 切换为英文(inline、ascii-inline、ascii-mode 都指的是英文)。如果同时定义了 rime-inline-predicates 变量,则当这两组函数都至少有一个断言成立时才会切换为英文。rime-predicate-after-alphabet-char-p
和rime-predicate-in-code-string-p
条件都会导 致不能正确的中英文混排。
(use-package rime
:custom
(rime-user-data-dir "~/Library/Rime/")
(rime-librime-root "~/.emacs.d/librime/dist")
(rime-emacs-module-header-root "/opt/homebrew/opt/emacs-plus@30/include")
:hook
(emacs-startup . (lambda () (setq default-input-method "rime")))
:bind
(
:map rime-active-mode-map
;; 在已经激活 Rime 候选菜单时,强制切换到英文直到按回车。
("M-j" . 'rime-inline-ascii)
:map rime-mode-map
;; 强制切换到中文模式.
("M-j" . 'rime-force-enable)
;; 下面这些快捷键需要发送给 rime 来处理, 需要与 default.custom.yaml 文件中的
;; key_binder/bindings配置相匹配。
("C-." . 'rime-send-keybinding) ;; 中英文切换
("C-+" . 'rime-send-keybinding) ;; 输入法菜单
("C-," . 'rime-send-keybinding) ;; 中英文标点切换
;;("C-," . 'rime-send-keybinding) ;; 全半角切换
)
:config
;; 在 modline 高亮输入法图标, 可用来快速分辨分中英文输入状态。
(setq mode-line-mule-info '((:eval (rime-lighter))))
;; 将如下快捷键发送给 rime,同时需要在 rime 的 key_binder/bindings 的部分配置才会生
;; 效。
(add-to-list 'rime-translate-keybindings "C-h") ;; 删除拼音字符
(add-to-list 'rime-translate-keybindings "C-d")
(add-to-list 'rime-translate-keybindings "C-k") ;; 删除误上屏的词语
(add-to-list 'rime-translate-keybindings "C-a") ;; 跳转到第一个拼音字符
(add-to-list 'rime-translate-keybindings "C-e") ;; 跳转到最后一个拼音字符support
;; shift-l, shift-r, control-l, control-r, 只有当使用系统 RIME 输入法时才有效。
(setq rime-inline-ascii-trigger 'shift-r)
;; 临时英文模式, 该列表中任何一个断言返回 t 时自动切换到英文。如果
;; rime-inline-predicates 不为空,则当其中任意一个断言也返回 t 时才会自动切换到英文
;; (inline 等效于 ascii-mode)。自定义 avy 断言函数。
(defun rime-predicate-avy-p () (bound-and-true-p avy-command))
(setq rime-disable-predicates
'(rime-predicate-ace-window-p
rime-predicate-hydra-p
;;rime-predicate-current-uppercase-letter-p
;; 在上一个字符是英文时才自动切换到英文,适合字符串中中英文混合的情况。
;;rime-predicate-in-code-string-after-ascii-p
;; 代码块内不能输入中文, 但注释和字符串不受影响。
;;rime-predicate-prog-in-code-p
;;rime-predicate-avy-p
))
(setq rime-show-candidate 'posframe)
(setq default-input-method "rime")
(setq rime-posframe-properties
(list :background-color "#333333"
:foreground-color "#dcdccc"
:internal-border-width 2))
;; 部分 mode 关闭 RIME 输入法。
(defadvice switch-to-buffer (after activate-input-method activate)
(if (or (string-match "vterm-mode" (symbol-name major-mode))
(string-match "dired-mode" (symbol-name major-mode))
(string-match "image-mode" (symbol-name major-mode))
(string-match "compilation-mode" (symbol-name major-mode))
(string-match "isearch-mode" (symbol-name major-mode))
(string-match "minibuffer-mode" (symbol-name major-mode)))
(activate-input-method nil)
(activate-input-method "rime"))))
本部分使用的三方包说明:
- vertico:minibuffer 补全;
- corfu:光标处补全;
- orderless:提供 flex/regex 过滤风格;
- consult:实时预览和高级过滤;
- embark:为 minibuffer/buffer 选中的内容提供快捷操作;
- marginalia:为候选者提供元数据(如 dired 风格的权限、修改时间等);
vertico 提供 minibuffer 自动补全(corfu 提供光标处的自动补全), 可以使用 orderless 过滤候选者:
C-] (abort-recursive-edit)
关闭 minibuffer 编辑和补全状态。M-RET(vertico-exit-input)
退出输入候选模式,直接使用输入的内容,可以是 file 或 buffer name。M-} (vertico-next-group)
选择候选者列表中的下一个分组,如不同的 file 或 project。TAB (vertico-insert)
插入当前选中的候选者;
(use-package vertico
:config
(setq vertico-count 15)
(vertico-mode 1)
(define-key vertico-map (kbd "<backspace>") #'vertico-directory-delete-char)
(define-key vertico-map (kbd "RET") #'vertico-directory-enter))
(use-package emacs
:init
;; minibuffer 不显示光标。
(setq minibuffer-prompt-properties '(read-only t cursor-intangible t face minibuffer-prompt))
(add-hook 'minibuffer-setup-hook #'cursor-intangible-mode)
;; M-x 只显示当前 mode 支持的命令。
(setq read-extended-command-predicate #'command-completion-default-include-p)
;; 开启 minibuffer 递归编辑。
(setq enable-recursive-minibuffers t))
corf 在光标处显示候选列表和文档, 可以使用 orderless 来过滤候选者:
(use-package corfu
:init
(global-corfu-mode 1)
(corfu-popupinfo-mode 1) ;; 显示候选者文档。
:bind
;; 滚动显示 corfu-popupinfo 内容的快捷键。
(:map corfu-popupinfo-map
("C-M-j" . corfu-popupinfo-scroll-up)
("C-M-k" . corfu-popupinfo-scroll-down))
:custom
(corfu-cycle t) ;; 自动轮转。
(corfu-auto t) ;; 自动补全(不需要按 TAB)。
(corfu-auto-prefix 2) ;; 触发自动补全的前缀长度。
(corfu-auto-delay 0.1) ;; 触发自动补全的延迟, 当满足前缀长度或延迟时, 都会自动补全。
(corfu-separator ?\s) ;; 使用 Orderless 过滤分隔符。
(corfu-preselect 'prompt) ;; Preselect the prompt
(corfu-scroll-margin 5)
(corfu-on-exact-match nil) ;; 默认不选中候选者(即使只有一个)。
(corfu-popupinfo-delay '(0.1 . 0.2)) ;; 候选者帮助文档显示延迟。
(corfu-popupinfo-max-width 80)
(corfu-popupinfo-max-height 50)
(corfu-popupinfo-direction '(force-right)) ;; 强制在右侧显示文档。
:config
(defun corfu-enable-always-in-minibuffer ()
(setq-local corfu-auto nil)
(corfu-mode 1))
(add-hook 'minibuffer-setup-hook #'corfu-enable-always-in-minibuffer 1)
;; corfu 支持 eshell 的 pcomplete 自动补全。
(add-hook 'eshell-mode-hook
(lambda ()
(setq-local corfu-auto nil)
(corfu-mode))))
;; 记录 minibuffer 和 corfu 补全历史,后续显示候选者时按照频率排序。
(use-package savehist
:hook (after-init . savehist-mode)
:config
(setq history-length 100)
(setq savehist-save-minibuffer-history t)
(setq savehist-autosave-interval 300)
(add-to-list 'savehist-additional-variables #'corfu-history)
(add-to-list 'savehist-additional-variables 'mark-ring)
(add-to-list 'savehist-additional-variables 'global-mark-ring)
(add-to-list 'savehist-additional-variables 'extended-command-history))
(use-package emacs
:init
;; 总是在弹出菜单中显示候选者。
(setq completion-cycle-threshold nil)
;; 使用 TAB 来 indentation + completion(completion-at-point 默认是 M-TAB) 。
(setq tab-always-indent 'complete))
orderless 补全风格:使用空格分割一个或多个匹配模式,模式无顺序,是 AND 关系。
orderless 默认使用 orderless-matching-styles
变量配置的 正则和字面量
匹配方式,通过给各模式指定前
缀或后缀字符, 也可以灵活指定其它匹配模式:
=
:orderless-literal
, 字面量匹配;- ~~~ :
orderless-flex
, 模糊匹配,如 abc 实际对应正则 a.*b.*c; ^
:orderless-literal-prefix
,前缀匹配;&
:orderless-annotation
,使用 marginalia 等提供的 meta 信息来过滤;,
:orderless-initialism
, 首字母缩写,如 ,abc 实际对应正则 \<a.*\<b.*\c;!
: makes the rest of the component match usingorderless-without-literal
, that is, both!bad and bad!
will match strings thatdo not contain the substring bad
.%
: makes the string match ignoring diacritics and similar inflections on characters (it uses the functionchar-fold-to-regexp
to do this).
! 只能对 字面量
匹配取反(orderless-without-literal) ,和其他 dispatch 字符连用时, ! 需要前缀形式,
如 !=.go
将不匹配含有字面量 .go 的候选者。
(use-package orderless
:demand t
:config
;; https://github.com/minad/consult/wiki#minads-orderless-configuration
(defun +orderless--consult-suffix ()
"Regexp which matches the end of string with Consult tofu support."
(if (and (boundp 'consult--tofu-char) (boundp 'consult--tofu-range))
(format "[%c-%c]*$"
consult--tofu-char
(+ consult--tofu-char consult--tofu-range -1))
"$"))
;; Recognizes the following patterns:
;; * .ext (file extension)
;; * regexp$ (regexp matching at end)
(defun +orderless-consult-dispatch (word _index _total)
(cond
;; Ensure that $ works with Consult commands, which add disambiguation suffixes
((string-suffix-p "$" word)
`(orderless-regexp . ,(concat (substring word 0 -1) (+orderless--consult-suffix))))
;; File extensions
((and (or minibuffer-completing-file-name
(derived-mode-p 'eshell-mode))
(string-match-p "\\`\\.." word))
`(orderless-regexp . ,(concat "\\." (substring word 1) (+orderless--consult-suffix))))))
;; 在 orderless-affix-dispatch 的基础上添加上面支持文件名扩展和正则表达式的 dispatchers。
(setq orderless-style-dispatchers
(list #'+orderless-consult-dispatch
#'orderless-affix-dispatch))
;; 自定义名为 +orderless-with-initialism 的 orderless 风格。
(orderless-define-completion-style +orderless-with-initialism
(orderless-matching-styles '(orderless-initialism orderless-literal orderless-regexp)))
;; 使用 orderless 和 Emacs 原生的 basic 补全风格,但 orderless 的优先级更高。
(setq completion-styles '(orderless basic))
(setq completion-category-defaults nil)
;; 设置 Emacs minibuffer 各 category 使用的补全风格。
(setq completion-category-overrides
'(
;; buffer name 补全
;;(buffer (styles +orderless-with-initialism))
;; 文件名和路径补全, partial-completion 提供了 wildcard 支持。
(file (styles partial-completion))
(command (styles +orderless-with-initialism))
(variable (styles +orderless-with-initialism))
(symbol (styles +orderless-with-initialism))
;; eglot will change the completion-category-defaults to flex, BAD!
;; https://github.com/minad/corfu/issues/136#issuecomment-eglot
;; 使用 M-SPC 来分隔光标处的多个筛选条件。
(eglot (styles . (orderless basic)))
(eglot-capf (styles . (orderless basic)))
))
;; 使用 SPACE 来分割过滤字符串。
(setq orderless-component-separator #'orderless-escapable-split-on-space))
- partial-completion 支持 shell wildcards 和部分文件路径,如 /u/s/l 对应 /usr/share/local;
- 已知的 completion categories;
在多个过滤模式间插入分隔符:
- 对于 buffer 中光标处的连续输入, 使用
M-SPC(corfu-insert-separator)
插入 orderless 分隔符; - 对于 minibuffer 区域的补全, 使用
SPC
(orderless 默认的分隔符) 分割多个过滤条件,如果要插入 SPC 本身,需要使用 \ 转义,如results\ no
.
consult:提供候选者实时过滤和预览等功能:
(use-package consult
:hook
(completion-list-mode . consult-preview-at-point-mode)
:init
;; 如果搜索字符少于 3,可以添加后缀 # 开始搜索,如 #gr#。
(setq consult-async-min-input 3)
;; 从头开始搜索(而非前位置)。
(setq consult-line-start-from-top t)
;; 寄存器预览。
(setq register-preview-function #'consult-register-format)
(advice-add #'register-preview :override #'consult-register-window)
:config
;; 不搜索 go vendor 目录。
(setq consult-ripgrep-args (concat consult-ripgrep-args " -g !vendor/"))
;; 按 C-l 才激活预览,否则 Buffer 列表中有大文件或远程文件时会卡住。
(setq consult-preview-key "C-l")
;; 不对 consult-line 结果进行排序(按行号排序)。
(consult-customize consult-line :prompt "Search: " :sort nil)
;; Buffer 列表中不显示的 Buffer 名称。
(mapcar
(lambda (pattern) (add-to-list 'consult-buffer-filter pattern))
'("\\*scratch\\*"
"\\*Warnings\\*"
"\\*helpful.*"
"\\*Help\\*"
"\\*Org Src.*"
"Pfuture-Callback.*"
"\\*epc con"
"\\*dashboard"
"\\*Ibuffer"
"\\*sort-tab"
"\\*Google Translate\\*"
"\\*straight-process\\*"
"\\*Native-compile-Log\\*"
"\\*EGLOT"
"[0-9]+.gpg")))
;; 执行 consult-line 命令时自动展开 org 内容。
;; https://github.com/minad/consult/issues/563#issuecomment-1186612641
(defun my/org-show-entry (fn &rest args)
(interactive)
(when-let ((pos (apply fn args)))
(when (derived-mode-p 'org-mode)
(org-fold-show-entry))))
(advice-add 'consult-line :around #'my/org-show-entry)
;; 显示 mode 相关的命令。
(global-set-key (kbd "C-c M-x") #'consult-mode-command)
;; 搜索 Emacs 各 package/mode 的 info 和 man 文档。
(global-set-key (kbd "C-c i") #'consult-info)
(global-set-key (kbd "C-c m") #'consult-man)
;; 使用 savehist 持久化保存的 minibuffer 历史。
(global-set-key (kbd "C-M-;") #'consult-complex-command)
;; consult-buffer 显示的 File 列表来源于变量 recentf-list。
(global-set-key (kbd "C-x b") #'consult-buffer)
(global-set-key (kbd "C-x 4 b") #'consult-buffer-other-window)
(global-set-key (kbd "C-x 5 b") #'consult-buffer-other-frame)
(global-set-key (kbd "C-x r b") #'consult-bookmark)
(global-set-key (kbd "C-x p b") #'consult-project-buffer)
(global-set-key (kbd "M-y") #'consult-yank-pop)
(global-set-key (kbd "M-Y") #'consult-yank-from-kill-ring)
(global-set-key (kbd "M-g g") #'consult-goto-line)
(global-set-key (kbd "M-g o") #'consult-outline)
;; 寄存器,保存 point、file、window、frame 的位置。
(global-set-key (kbd "C-'") #'consult-register-store)
(global-set-key (kbd "C-M-'") #'consult-register)
;; 显示编译错误列表。
(global-set-key (kbd "M-g e") #'consult-compile-error)
;; 显示 flymake 诊断错误列表。
(global-set-key (kbd "M-g f") #'consult-flymake)
;; consult-buffer 默认已包含 recent file。
;;(global-set-key (kbd "M-g r") #'consult-recent-file)
(global-set-key (kbd "M-g m") #'consult-mark)
(global-set-key (kbd "M-g k") #'consult-global-mark)
;; 预览当前 buffer 的 imenu。
(global-set-key (kbd "M-g i") #'consult-imenu)
;; 预览当前 project 打开的所有 buffer 的 imenu。
(global-set-key (kbd "M-g I") #'consult-imenu-multi)
;; 搜索文件内容。
(global-set-key (kbd "M-s g") #'consult-grep)
(global-set-key (kbd "M-s G") #'consult-git-grep)
(global-set-key (kbd "M-s r") #'consult-ripgrep)
;; 搜索文件名(正则匹配)。
(global-set-key (kbd "M-s d") #'consult-find)
(global-set-key (kbd "M-s D") #'consult-locate)
;; 搜索当前 buffer
(global-set-key (kbd "M-s l") #'consult-line)
(global-set-key (kbd "M-s M-l") #'consult-line)
;; 搜索多个 buffer,默认为 project 的多个 buffers。
;; 如果使用前缀参数,则搜索所有 buffers。
(global-set-key (kbd "M-s L") #'consult-line-multi)
;; Isearch 集成。
(global-set-key (kbd "M-s e") #'consult-isearch-history)
;;:map isearch-mode-map
(define-key isearch-mode-map (kbd "M-e") #'consult-isearch-history)
(define-key isearch-mode-map (kbd "M-s e") #'consult-isearch-history)
(define-key isearch-mode-map (kbd "M-s l") #'consult-line)
(define-key isearch-mode-map (kbd "M-s L") #'consult-line-multi)
;; Minibuffer 历史。
;;:map minibuffer-local-map)
(define-key minibuffer-local-map (kbd "M-s") #'consult-history)
(define-key minibuffer-local-map (kbd "M-r") #'consult-history)
;; 使用 consult 来预览 xref 的引用定义和跳转。
(setq xref-show-xrefs-function #'consult-xref)
(setq xref-show-definitions-function #'consult-xref)
;; 限制 xref history 仅局限于当前窗口(默认全局)。
(setq xref-history-storage 'xref-window-local-history)
;; 在其它窗口查看定义。
(global-set-key (kbd "C-M-.") 'xref-find-definitions-other-window)
embark:为选中的内容提供快捷操作命令:
(use-package embark
:init
;; 使用 C-h 显示 key preifx 绑定。
(setq prefix-help-command #'embark-prefix-help-command)
:config
(setq embark-prompter 'embark-keymap-prompter)
(global-set-key (kbd "C-;") #'embark-act) ;; embark-dwim
;; 根据当前 buffer 的 mode,显示可以使用的快捷键。
(define-key global-map [remap describe-bindings] #'embark-bindings))
;; embark-consult 支持 embark 和 consult 集成,使用 wgrep 编辑 consult grep/line 的 export 的结果。
(use-package embark-consult
:after (embark consult)
:hook (embark-collect-mode . consult-preview-at-point-mode))
;; 编辑 grep buffers, 可以和 consult-grep 和 embark-export 联合使用。
(use-package wgrep
:config
;; 执行 wgre-finished-edit 时保存所有修改的 buffer。
(setq wgrep-auto-save-buffer t)
(setq wgrep-change-readonly-file t))
marginalia:为候选者提供元数据(如 dired 风格的权限、修改时间等):
(use-package marginalia
:init
;; 显示绝对时间。
(setq marginalia-max-relative-age 0)
(marginalia-mode))
(use-package org
:config
(setq
org-ellipsis "..." ;; " ⭍"
;; 使用 UTF-8 显示 LaTeX 或 \xxx 特殊字符, M-x org-entities-help 查看所有特殊字符。
org-pretty-entities t
org-highlight-latex-and-related '(latex)
;; 只显示而不处理和解释 latex 标记,例如 \xxx 或 \being{xxx}, 避免 export pdf 时出错。
org-export-with-latex 'verbatim
org-export-with-broken-links 'mark
;; export 时不处理 super/sub scripting, 等效于 #+OPTIONS: ^:nil 。
org-export-with-sub-superscripts nil
org-export-default-language "zh-CN"
org-export-coding-system 'utf-8
;; 使用 R_{s} 形式的下标(默认是 R_s, 容易与正常内容混淆) 。
org-use-sub-superscripts nil
;; 文件链接使用相对路径, 解决 hugo 等 image 引用的问题。
org-link-file-path-type 'relative
org-html-validation-link nil
;; 关闭鼠标点击链接。
org-mouse-1-follows-link nil
org-hide-emphasis-markers t
org-hide-block-startup t
org-hidden-keywords '(title)
org-hide-leading-stars t
org-cycle-separator-lines 2
org-cycle-level-faces t
org-n-level-faces 4
org-indent-indentation-per-level 2
;; 内容缩进与对应 headerline 一致。
org-adapt-indentation t
org-list-indent-offset 2
;; 代码块缩进。
org-src-preserve-indentation t
org-edit-src-content-indentation 0
;; TODO 状态更新记录到 LOGBOOK Drawer 中。
org-log-into-drawer t
;; TODO 状态更新时记录 note.
org-log-done 'note ;; note, time
;; 不显示图片(手动点击显示更容易控制大小)。
org-startup-with-inline-images nil
org-startup-folded 'content
org-cycle-inline-images-display nil
;; 如果对 headline 编号则 latext 输出时会导致 toc 缺失,故关闭。
org-startup-numerated nil
org-startup-indented t
;; 先从 #+ATTR.* 获取宽度,如果没有设置则默认为 300 。
org-image-actual-width '(300)
;; org-timer 到期时发送声音提示。
org-clock-sound t
;; 关闭容易误按的 archive 命令。
org-archive-default-command nil
;; 不自动对齐 tag。
org-tags-column 0
org-auto-align-tags nil
;; 显示不可见的编辑。
org-catch-invisible-edits 'show-and-error
org-fold-catch-invisible-edits t
;; 支持 ID property 作为 internal link target(默认是 CUSTOM_ID property)
org-id-link-to-org-use-id t
org-M-RET-may-split-line nil
;; 关闭频繁弹出的 org-element-cache 警告 buffer 。
org-element-use-cache nil
org-todo-keywords
'((sequence "TODO(t!)" "DOING(d@)" "|" "DONE(D)")
(sequence "WAITING(w@/!)" "NEXT(n!/!)" "SOMEDAY(S)" "|" "CANCELLED(c@/!)"))
org-special-ctrl-a/e t
org-insert-heading-respect-content t)
;;(add-hook 'org-mode-hook 'turn-on-auto-fill)
(add-hook 'org-mode-hook (lambda () (display-line-numbers-mode 0))))
(global-set-key (kbd "C-c l") #'org-store-link)
(global-set-key (kbd "C-c a") #'org-agenda)
(global-set-key (kbd "C-c c") #'org-capture)
(global-set-key (kbd "C-c b") #'org-switchb)
;; 关闭 org-mode 的 C-c C-j 快捷键, 与 journal 冲突.
(define-key org-mode-map (kbd "C-c C-j") nil)
;; 关闭 org-mode 的 C-' 对应的 org-cycle-agenda-files 命令, 与 consult-register-store 冲突。
(define-key org-mode-map (kbd "C-'") nil)
;; 光标位于 src block 中执行 C-c C-f 时自动格式化 block 中代码。
(defun my/format-src-block ()
"Formats the code in the current src block."
(interactive)
(org-edit-special)
(indent-region (point-min) (point-max))
(org-edit-src-exit))
(defun my/org-mode-keys ()
"Modify keymaps used in org-mode."
(let ((map (if (org-in-src-block-p)
org-src-mode-map
org-mode-map)))
(define-key map (kbd "C-c C-f") 'my/format-src-block)))
(add-hook 'org-mode-hook 'my/org-mode-keys)
;; 建立 org 相关目录。
(dolist (dir '("~/docs/org" "~/docs/org/journal"))
(unless (file-directory-p dir)
(make-directory dir)))
配置 babel:
;; 关闭 C-c C-c 触发执行代码.
(setq org-babel-no-eval-on-ctrl-c-ctrl-c t)
;; 确认执行代码的操作。
(setq org-confirm-babel-evaluate t)
;; 使用语言的 mode 来格式化代码.
(setq org-src-fontify-natively t)
;; 使用各语言的 Major Mode 来编辑 src block。
(setq org-src-tab-acts-natively t)
;; yaml 从外部的 yaml-mode 切换到内置的 yaml-ts-mode,告诉 babel 使用该内置 mode,否则编辑 yaml src
;; block 时提示找不到 yaml-mode。
(add-to-list 'org-src-lang-modes '("yaml" . yaml-ts))
(add-to-list 'org-src-lang-modes '("cue" . cue))
(require 'org)
;; org bable 完整支持的语言列表(ob- 开头的文件):
;; https://git.savannah.gnu.org/cgit/emacs/org-mode.git/tree/lisp 对于官方不支持的语言,可以通过
;; use-pacakge 来安装。
(use-package ob-go)
(use-package ob-rust)
(org-babel-do-load-languages
'org-babel-load-languages
'((shell . t)
(js . t)
(makefile . t)
(go . t)
(emacs-lisp . t)
(rust . t)
(python . t)
(C . t) ;; 支持 C/C++/D
(java . t)
(awk . t)
(css . t)))
(use-package org-contrib)
org-mode 内容居中显示:
(use-package olivetti
:config
;; 文本区域宽度,超过后自动折行。
(setq-default olivetti-body-width 130)
(add-hook 'org-mode-hook 'olivetti-mode))
;; fill-column 值要小于 olivetti-body-width 才能正常折行。
(setq-default fill-column 100)
;; 由于 auto-fill 可能会打乱代码的字符串和注释,故为 prog-mode/text-mode 等全局关闭 auto-fill。
;;(add-hook 'text-mode-hook 'turn-on-auto-fill)
org-modern 和 org-appear 美化:
(use-package org-modern
:after (org)
:config
;; 各种符号字体:https://github.com/rime/rime-prelude/blob/master/symbols.yaml
;;(setq org-modern-star '("◉" "○" "✸" "✿" "✤" "✜" "◆" "▶"))
(setq org-modern-star '("⚀" "⚁" "⚂" "⚃" "⚄" "⚅"))
(setq org-modern-block-fringe nil)
(setq org-modern-block-name
'((t . t)
("src" "»" "«")
("SRC" "»" "«")
("example" "»–" "–«")
("quote" "❝" "❞")))
;; 美化表格。
(setq org-modern-table t)
(setq org-modern-list
'(
(?* . "✤")
(?+ . "▶")
(?- . "◆")))
(with-eval-after-load 'org (global-org-modern-mode)))
;; 显示转义字符。
(use-package org-appear
:custom
(org-appear-autolinks t)
:hook (org-mode . org-appear-mode))
org-download:拖拽图片或将剪贴板中图片插入到 org-mode buffer 中(使用 pngpaste 命令):
- 需要编译 Emacs 时指定
--with-imagemagick
参数,Emacs 使用 imagemagick 命令来实时转换图片大小。
(use-package org-download
:config
;; 保存路径包含 /static/ 时, ox-hugo 在导出时保留后面的目录层次。
(setq-default org-download-image-dir "./static/images/")
(setq org-download-method 'directory
org-download-display-inline-images 'posframe
org-download-screenshot-method "pngpaste %s"
org-download-image-attr-list '("#+ATTR_HTML: :width 400 :align center"))
(add-hook 'dired-mode-hook 'org-download-enable)
(org-download-enable)
(global-set-key (kbd "<f6>") #'org-download-screenshot)
;; 不添加 #+DOWNLOADED: 注释。
(setq org-download-annotate-function (lambda (link) (previous-line 1) "")))
tex 和 PDF 导出:
;; 将安装的 tex 二进制目录添加到 PATH 环境变量和 exec-path 变量中,Emacs 执行 xelatex 命令时使用。
(setq my-tex-path "/Library/TeX/texbin")
(setenv "PATH" (concat my-tex-path ":" (getenv "PATH")))
(setq exec-path (cons my-tex-path exec-path))
;; engrave-faces 比 minted 渲染速度更快。
(use-package engrave-faces
:after ox-latex
:config
(require 'engrave-faces-latex)
(setq org-latex-src-block-backend 'engraved)
;; 代码块左侧添加行号。
(add-to-list 'org-latex-engraved-options '("numbers" . "left"))
;; 代码块主题。
(setq org-latex-engraved-theme 'ef-light))
(defun my/export-pdf (backend)
(progn
;;(setq org-export-with-toc nil)
(setq org-export-headline-levels 2))
)
(add-hook 'org-export-before-processing-functions #'my/export-pdf)
;; ox- 为 org-mode 的导出后端包的惯例前缀。
;;(use-package ox-reveal) ;; reveal.js
(use-package ox-gfm :defer t) ;; github flavor markdown
(require 'ox-latex)
(with-eval-after-load 'ox-latex
;; latex image 的默认宽度, 可以通过 #+ATTR_LATEX :width xx 配置。
(setq org-latex-image-default-width "0.7\\linewidth")
;; 使用 booktabs style 来显示表格,例如支持隔行颜色, 这样 #+ATTR_LATEX: 中不需要添加 :booktabs t。
(setq org-latex-tables-booktabs t)
;; 不保存 LaTeX 日志文件(调试时设置为 nil)。
(setq org-latex-remove-logfiles t)
;; 使用支持中文的 xelatex。
(setq org-latex-pdf-process '("latexmk -xelatex -quiet -shell-escape -f %f"))
(add-to-list 'org-latex-classes
'("ctexart"
"\\documentclass[lang=cn,11pt,a4paper,table]{ctexart}
[NO-DEFAULT-PACKAGES]
[PACKAGES]
[EXTRA]"
("\\section{%s}" . "\\section*{%s}")
("\\subsection{%s}" . "\\subsection*{%s}")
("\\subsubsection{%s}" . "\\subsubsection*{%s}")
("\\paragraph{%s}" . "\\paragraph*{%s}")
("\\subparagraph{%s}" . "\\subparagraph*{%s}"))))
;; org export html 格式时需要 htmlize.el 包来格式化代码。
(use-package htmlize)
自定义导出 PDF 的 LaTeX 样式 mystyle.sty: 对于表格,如果列内容过宽则导出的 PDF 中该列的内容会被截断, 可以为表格设置如下属性,将超宽列 align 设置为 X 来解决:
#+ATTR_LATEX: :environment tabularx :booktabs t :width \linewidth :align l|l|X
\usepackage{wallpaper} % 显示封面图片或页面图片。
\usepackage{color}
\usepackage{xcolor}
\definecolor{winered}{rgb}{0.5,0,0}
\definecolor{lightgrey}{rgb}{0.9,0.9,0.9}
\definecolor{tableheadcolor}{gray}{0.92}
\definecolor{commentcolor}{RGB}{0,100,0}
\definecolor{frenchplum}{RGB}{190,20,83}
% 提示 title
\usepackage[explicit]{titlesec}
% 每个 chapter 另起一页
\newcommand{\sectionbreak}{\clearpage}
\usepackage{titling}
\setlength{\droptitle}{-6em}
% 超链接和书签
\usepackage[colorlinks]{hyperref}
\hypersetup{
pdfborder={0 0 0},
colorlinks=true,
bookmarksopen=true,
bookmarksnumbered=true, % 书签目录显示编号。
linkcolor={winered},
urlcolor={winered},
filecolor={winered},
citecolor={winered},
linktoc=all}
% 安装 noto-cjk 中文字体: git clone https://github.com/googlefonts/noto-cjk.git
\usepackage{fontspec}
\usepackage[utf8x]{inputenc}
\setmainfont{Noto Serif SC}
\setsansfont{Noto Sans SC}[Scale=MatchLowercase]
\setmonofont{Noto Sans Mono CJK SC}[Scale=MatchLowercase]
\setCJKmainfont[BoldFont=Noto Serif SC]{Noto Serif SC}
\setCJKsansfont{Noto Sans SC}
\setCJKmonofont{Noto Sans Mono CJK SC}
\XeTeXlinebreaklocale "zh"
\XeTeXlinebreakskip = 0pt plus 1pt minus 0.1pt
% 添加 email 命令。
\newcommand\email[1]{\href{mailto:#1}{\nolinkurl{#1}}}
% sidewaytable 依赖 rotfloat
\usepackage {rotfloat}
% tabularx 的特殊 align 参数 X 用来对指定列内容自动换行,否则该列内容有可能被截断,
% 解决办法是:在 org-mode 表格前需要加如下属性:
% #+ATTR_LATEX: :environment tabularx :booktabs t :width \linewidth :align l|X
\usepackage{tabularx}
% 美化表格显示效果
\usepackage{booktabs}
% 表格隔行颜色, {1} 开始行, {lightgrep} 奇数行颜色, {} 偶数行颜色(空表示白色)
\rowcolors{1}{lightgrey}{}
\usepackage{parskip}
\setlength{\parskip}{1em}
\setlength{\parindent}{0pt}
\usepackage{etoolbox}
\usepackage{calc}
\usepackage[scale=0.85]{geometry}
%\setlength{\headsep}{5pt}
\usepackage{amsthm}
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{indentfirst}
\usepackage{multicol}
\usepackage{multirow}
\usepackage{linegoal}
\usepackage{graphicx}
\usepackage{fancyvrb}
\usepackage{abstract}
\usepackage{hologo}
\linespread{1}
\graphicspath{{image/}{figure/}{fig/}{img/}{images/}}
\usepackage[font=small,labelfont={bf}]{caption}
\captionsetup[table]{skip=3pt}
\captionsetup[figure]{skip=3pt}
% 下划线、强调和删除线等
\usepackage[normalem]{ulem}
% 列表
\usepackage[shortlabels,inline]{enumitem}
\setlist{nolistsep}
% xeCJK 默认会把黑点用汉字显示,而 Noto 没有这个字体,所以显示效果为一个小点。解决办法是将它设置为
% \bullet, 这样显示为实心黑点。Windows 带的楷体、仿宋没有这个问题。
\setlist[itemize]{label=$\bullet$}
% 或者:
%\renewcommand\labelitemi{\ensuremath{\bullet}}
创建一个 tempel 模板,便于在 org-mode 文件中快速插入导出 PDF 的 tex 配置参数:
- 如果生成的 pdf 不显示目录,检查文档
#+OPTIONS
参数中的toc:nil
和num: 2
是否生效;
(my-latex "#+DATE: " (format-time-string "%Y-%m-%d %a") n
"#+SUBTITLE: 内部资料,注意保密!
#+AUTHOR: 张俊([email protected])
# 中文语言环境(目录等用中文显示)。
#+LANGUAGE: zh-CN
# 不自动输出 titile 和 toc,后续 latext mystyle 中定制输出。
# 但是需要明确通过 num 控制输出的目录级别。
#+OPTIONS: prop:t title:nil num:2 toc:nil ^:nil
#+LATEX_COMPILER: xelatex
#+LATEX_CLASS: ctexart
#+LATEX_HEADER: \\usepackage{/Users/alizj/emacs/mystyle}
# 定制 PDF 封面和目录。
% 封面页
\\begin{titlepage}
% 插入标题
\\maketitle
% 插入封面图
%\\ThisCenterWallPaper{0.4}{/path/to/image.png}
% 封面页不编号
\\noindent\\fboxsep=0pt
\\setcounter{page}{0}
\\thispagestyle{empty}
\\end{titlepage}
% 摘要页
\\begin{abstract}
这是一个摘要。
\\end{abstract}
% 目录页
\\newpage
\\tableofcontents
\\newpage
")
slide 演示:org-tree-slide 不再活跃维护了,dslide 是它的的替代品。
(use-package dslide
:vc(:url "https://github.com/positron-solutions/dslide.git")
:hook
((dslide-start
.
(lambda ()
(org-fold-hide-block-all)
(setq-default x-stretch-cursor -1)
(redraw-display)
(blink-cursor-mode -1)
(setq cursor-type 'bar)
;;(org-display-inline-images)
;;(hl-line-mode -1)
(text-scale-increase 2)
(read-only-mode 1)))
(dslide-stop
.
(lambda ()
(blink-cursor-mode +1)
(setq-default x-stretch-cursor t)
(setq cursor-type t)
(text-scale-increase 0)
;;(hl-line-mode 1)
(read-only-mode -1))))
:config
(setq dslide-margin-content 0.5)
(setq dslide-animation-duration 0.5)
(setq dslide-margin-title-above 0.3)
(setq dslide-margin-title-below 0.3)
(setq dslide-header-email nil)
(setq dslide-header-date nil)
(define-key org-mode-map (kbd "<f8>") #'dslide-deck-start)
(define-key dslide-mode-map (kbd "<f9>") #'dslide-deck-stop))
journal 日记:
(use-package org-journal
:commands org-journal-new-entry
:bind (("C-c j" . org-journal-new-entry))
:init
(setq org-journal-prefix-key "C-c j")
(defun org-journal-save-entry-and-exit()
(interactive)
(save-buffer)
(kill-buffer-and-window))
:config
(define-key org-journal-mode-map (kbd "C-c C-e") #'org-journal-save-entry-and-exit)
(define-key org-journal-mode-map (kbd "C-c C-j") #'org-journal-new-entry)
(global-set-key (kbd "C-c C-j") #'org-journal-new-entry)
;; 设置日志文件头。
(defun org-journal-file-header-func (time)
"Custom function to create journal header."
(concat
(pcase org-journal-file-type
(`daily "#+TITLE: Daily Journal\n#+STARTUP: showeverything")
(`weekly "#+TITLE: Weekly Journal\n#+STARTUP: folded")
(`monthly "#+TITLE: Monthly Journal\n#+STARTUP: folded")
(`yearly "#+TITLE: Yearly Journal\n#+STARTUP: folded"))))
(setq org-journal-file-header 'org-journal-file-header-func)
(setq org-journal-file-type 'daily) ;; 按天记录。
(setq org-journal-dir "~/docs/org/journal")
(setq org-journal-find-file 'find-file)
;; 加密日记文件。
(setq org-journal-enable-encryption t)
(setq org-journal-encrypt-journal t)
(defun my-old-carryover (old_carryover)
(save-excursion
(let ((matcher (cdr (org-make-tags-matcher org-journal-carryover-items))))
(dolist (entry (reverse old_carryover))
(save-restriction
(narrow-to-region (car entry) (cadr entry))
(goto-char (point-min))
(org-scan-tags '(lambda ()
(org-set-tags ":carried:"))
matcher org--matcher-tags-todo-only))))))
(setq org-journal-handle-old-carryover 'my-old-carryover))
创建一个 templ 模板,便于在文件开头添加内容,可避免每次打开时提示选择 GPG key:
;; 插入自己的 GnuPG 加密 key。 (my-gpg "# -*- mode:org; epa-file-encrypt-to: (\"[email protected]\") -*-")
ox-hugo 博客:
(use-package ox-hugo
:demand
:config
(setq org-hugo-base-dir (expand-file-name "~/blog/blog.opsnull.com/"))
(setq org-hugo-section "posts")
(setq org-hugo-front-matter-format "yaml")
(setq org-hugo-export-with-section-numbers t)
(setq org-export-backends '(go md gfm html latex man hugo))
(setq org-hugo-auto-set-lastmod t))
缩进层次可视化:
(use-package indent-bars
:vc (:url "https://github.com/jdtsmith/indent-bars")
:config
(require 'indent-bars-ts)
:custom
(indent-bars-treesit-support t)
(indent-bars-treesit-ignore-blank-lines-types '("module"))
(indent-bars-treesit-scope
'((python
function_definition
class_definition
for_statement
if_statement
with_statement
while_statement)))
:hook
((python-base-mode
yaml-ts-mode
json-ts-mode
js-ts-mode) . indent-bars-mode))
缩进风格:c/c++/go-mode/kernel 均使用 tab 缩进:
;; 默认不使用 tab 缩进。
;;(setq indent-tabs-mode t)
(setq c-ts-mode-indent-offset 8)
(setq c-ts-common-indent-offset 8)
(setq c-basic-offset 8)
;; kernel 风格:table 和 offset 都是 tab 缩进,而且都是 8 字符。
;; https://www.kernel.org/doc/html/latest/process/coding-style.html
(setq c-default-style "linux")
(setq tab-width 8)
;; 关闭 electric-indent-mode,同时重新定义 C-j 和 C-m 快捷键,使用 major-mode 的缩进规则。
;;(electric-indent-mode 0)
彩色括号:
(use-package rainbow-delimiters
:hook (prog-mode . rainbow-delimiters-mode))
高亮匹配的括号:
(use-package paren
:hook (after-init . show-paren-mode)
:init
(setq show-paren-delay 0.1)
(setq show-paren-when-point-inside-paren t
show-paren-when-point-in-periphery t)
(setq show-paren-style 'parenthesis) ;; parenthesis, expression
(set-face-attribute 'show-paren-match nil :weight 'extra-bold))
智能补全括号:
(electric-pair-mode 1)
(setq electric-pair-pairs
'(
(?\" . ?\")
(?\{ . ?\})))
(setq electric-pair-preserve-balance t
electric-pair-delete-adjacent-pairs t
electric-pair-skip-self 'electric-pair-default-skip-self
electric-pair-open-newline-between-pairs t)
project 使用 top-down 方式来检查项目路径中是否存在 .project 文件,所以在上层各路径的 目录中不应该存在 .project 文件,否则会导致判断失败。
- 手动标记项目根目录:在目录下创建
.project
文件 - 查看当前项目的 project root:
(project-current)
- 手动添加 project 目录:
M-x project-remember-projects-under
- 调试目录的 project root 识别情况:
(my/project-try-local "/path/to/directory")
(use-package project
:custom
(project-switch-commands
'(
(consult-project-buffer "buffer" ?b)
(project-dired "dired" ?d)
(magit-project-status "magit status" ?g)
(project-find-file "find file" ?p)
(consult-ripgrep "rigprep" ?r)
(vterm-toggle-cd "vterm" ?t)))
(project-vc-merge-submodules nil)
:config
;; project-find-file 忽略的目录或文件列表。
(add-to-list 'vc-directory-exclusion-list "vendor") ;; go
(add-to-list 'vc-directory-exclusion-list "node_modules") ;; node
(add-to-list 'vc-directory-exclusion-list "target") ;; rust
)
(defun my/project-try-local (dir)
"Determine if DIR is a non-Git project."
(catch 'ret
(let ((pr-flags '(
;; 顺着目录 top-down 查找第一个匹配的文件。所以中间目录不能有
;; .project 等文件,否则判断 project root 错误。
("go.mod" "Cargo.toml" "pom.xml" "package.json" ".project" )
;; 以下文件容易导致 project root 判断错误, 故不添加。
;; ("Makefile" "README.org" "README.md")
)))
(dolist (current-level pr-flags)
(dolist (f current-level)
(when-let ((root (locate-dominating-file dir f)))
(throw 'ret (cons 'local root))))))))
(setq project-find-functions '(my/project-try-local project-try-vc))
(cl-defmethod project-root ((project (head local)))
(cdr project))
(defun my/project-discover ()
(interactive)
;; 去掉 "~/go/src/k8s.io/*" 目录。
(dolist (search-path
'("~/go/src/github.com/*"
"~/go/src/github.com/*/*"
"~/go/src/gitlab.*/*/*"))
(dolist (file (file-expand-wildcards search-path))
(when (file-directory-p file)
(message "dir %s" file)
;; project-remember-projects-under 列出 file 下的目录, 分别加到
;; project-list-file 中。
(project-remember-projects-under file nil)
(message "added project %s" file)))))
;; 不将 tramp 项目记录到 projects 文件中,防止 emacs-dashboard 启动时检查 project 卡
;; 住。
(defun my/project-remember-advice (fn pr &optional no-write)
(let* ((remote? (file-remote-p (project-root pr)))
(no-write (if remote? t no-write)))
(funcall fn pr no-write)))
(advice-add 'project-remember-project :around 'my/project-remember-advice)
(setq vc-follow-symlinks t)
;; 自动 revert buffer,确保 modeline 上的分支名正确。
(setq auto-revert-check-vc-info t)
(use-package magit
:custom
;; 在当前 window 中显示 magit buffer。
(magit-display-buffer-function #'magit-display-buffer-same-window-except-diff-v1)
(magit-log-arguments '("-n256" "--graph" "--decorate" "--color"))
;; 按照 word 展示 diff。
(magit-diff-refine-hunk t)
(magit-clone-default-directory "~/go/src/")
:config
;; diff org-mode 时展开内容。
(add-hook 'magit-diff-visit-file-hook (lambda() (when (derived-mode-p 'org-mode)(org-fold-show-entry)))))
git-link 为光标位置生成 git 仓库 URL:
(use-package git-link
:config
(setq git-link-use-commit t)
;; 重写 gitlab 的 format 字符串以匹配内部系统。
(defun git-link-commit-gitlab (hostname dirname commit)
(format "https://%s/%s/commit/%s" hostname dirname commit))
(defun git-link-gitlab (hostname dirname filename branch commit start end)
(format "https://%s/%s/blob/%s/%s" hostname dirname
(or branch commit)
(concat filename
(when start
(concat "#"
(if end
(format "L%s-%s" start end)
(format "L%s" start))))))))
treesit-auto 自动安装 grammer 和自动将 major-mode remap 到对应的 xx-ts-mode 上。具体
参考变 treesit-auto-recipe-list
:
(use-package treesit-auto
:demand t
:config
(setq treesit-auto-install 'prompt)
(global-treesit-auto-mode))
grammer 被安装到 ~/.emacs.d/tree-sitter
, 如
~/.emacs.d/tree-sitter/libtree-sitter-python.dylib
- 执行
M-x treesit-auto-install-all
来安装所有的 treesit modules。 - 如果要重新安装(升级) grammer, 需要先删除 dylib 文件或 tree-sitter 目录, 重启 emacs
后再执行
M-x treesit-auto-install-all
.
代码折叠:
(use-package treesit-fold
:vc (:url "https://github.com/emacs-tree-sitter/treesit-fold")
:config
(global-set-key (kbd "C-c f f") 'treesit-fold-close)
(global-set-key (kbd "C-c f o") 'treesit-fold-open)
(global-set-key (kbd "C-c f O") 'treesit-fold-open-recursively)
(global-set-key (kbd "C-c f F") 'treesit-fold-close-all)
(global-set-key (kbd "C-c f u") 'treesit-fold-open-all)
(global-set-key (kbd "C-c f t") 'treesit-fold-toggle))
flymake 为当前 buffer 提供错误检查/诊断功能,它在如下情况检查(诊断)buffer 是否有错 误,错误信息直接显示在 buffer 区域,并发送给 eldoc:
- 执行
M-x flymake-start
; - 超过
flymake-no-changes-timeout(默认 0.5)
; - 保存 buffer 时 (除非设置 flymake-start-on-save-buffer 为 nil);
将 flymake-no-changes-timeout 设置为 nil 后,eglot 不会显示 LSP 实时诊断消息,而是当 保存 buffer 后经过 eglot-send-changes-idle-time 时间后才显示诊断消息,这样可以避免显 示编码过程无意义的错误。
(use-package flymake
:config
;; 不自动检查 buffer 错误。
(setq flymake-no-changes-timeout nil)
;; 在行尾显示诊断消息(Emacs 30 开始支持), 'short 只显示一条最重要信息,t 显示所有
;; 信息。
(setq flymake-show-diagnostics-at-end-of-line 'short)
;; 如果 buffer 出现错误的诊断消息,执行 flymake-start 重新触发诊断。
(define-key flymake-mode-map (kbd "C-c C-c") #'flymake-start)
;; 显示诊断错误列表
(global-set-key (kbd "C-s-l") #'consult-flymake)
(define-key flymake-mode-map (kbd "C-s-n") #'flymake-goto-next-error)
(define-key flymake-mode-map (kbd "C-s-p") #'flymake-goto-prev-error))
;; 解决 flymake-no-changes-timeout 为 nil 时诊断延迟的问题。
;;; https://github.com/joaotavora/eglot/issues/1296
;; (cl-defmethod eglot-handle-notification :after
;; (_server (_method (eql textDocument/publishDiagnostics)) &key uri
;; &allow-other-keys)
;; (when-let ((buffer (find-buffer-visiting (eglot-uri-to-path uri))))
;; (with-current-buffer buffer
;; (if (and (eq nil flymake-no-changes-timeout)
;; (not (buffer-modified-p)))
;; (flymake-start t)))))
elgot 使用 Emacs 内置的 flymake(而非 flycheck)、xref、eldoc、project 等包。
eglot 使用 flymake 来接收和显示 LSP Server 发送的 publishDiagnostics 事件,这是通过向 flymake-diagnostic-functions hook 添加 ‘eglot-flymake-backend 实现的。
eglot 默认将 flymake 的 backend 清空,只保留 eglot 自身,可以通过配置 (add-to-list
'eglot-stay-out-of 'flymake)
来关闭 eglot 对 flymake 的清空行为,这样可以使用自定义的
flymake backends,但后续需要添加 hook 来手动启动和配置 eglot-flymake-backend。
(use-package eglot
:demand
:after (flymake)
:preface
(defun my/eglot-eldoc ()
;; eglot will change the completion-category-defaults to flex, BAD!
;; https://github.com/minad/corfu/issues/136#issuecomment-eglot
;; 这里将 completion-category-defaults 设置为 nil,然后在 completion-category-overrides
;; 中设置 eglot 使用 orderless 补全风格。
(setq completion-category-defaults nil)
;; 在 eldoc buffer 开始优先显示 flymake 诊断信息。
(setq eldoc-documentation-functions
(cons #'flymake-eldoc-function
(remove #'flymake-eldoc-function eldoc-documentation-functions)))
)
:hook ((eglot-managed-mode . my/eglot-eldoc))
:bind
(:map eglot-mode-map
("C-c C-a" . eglot-code-actions)
("C-c C-f" . eglot-format-buffer)
("C-c C-r" . eglot-rename)
("C-c C-c" . flymake-start)
("C-c C-d" . eldoc))
:config
;; 将 eglot-events-buffer-size 设置为 0 后将关闭显示 *EGLOT event* bufer,不便于调
;; 试问题。也不能设置的太大,否则可能影响性能。
(setq eglot-events-buffer-size (* 1024 1024 1))
;; 将 flymake-no-changes-timeout 设置为 nil 后,eglot 保存 buffer 内容后,经过 idle
;; time 才会向LSP 发送诊断请求。
(setq eglot-send-changes-idle-time 0.1)
;; 当最后一个源码 buffer 关闭时自动关闭 eglot server。
(customize-set-variable 'eglot-autoshutdown t)
(customize-set-variable 'eglot-connect-timeout 60)
;;不给所有 prog-mode 都开启 eglot,否则当它没有 language server 时 eglot 报错。
;;
;;由于 treesit-auto 已经对 major-mode 做了 remap ,需要对 xx-ts-mode-hook 添加 hook,
;;而不是以前的 xx-mode-hook, 否则添加到 xx-mode-hook 的内容不会被自动执行。
(add-hook 'c-ts-mode-hook #'eglot-ensure)
(add-hook 'go-ts-mode-hook #'eglot-ensure)
(add-hook 'bash-ts-mode-hook #'eglot-ensure)
(add-hook 'python-mode-hook #'eglot-ensure)
(add-hook 'python-ts-mode-hook #'eglot-ensure)
(add-hook 'rust-ts-mode-hook #'eglot-ensure)
(add-hook 'rust-mode-hook #'eglot-ensure)
(add-hook 'yaml-mode-hook #'eglot-ensure)
(add-hook 'yaml-ts-mode-hook #'eglot-ensure)
(setq eglot-ignored-server-capabilities
'(
;;:hoverProvider ;; 显示光标位置信息。
;;:documentHighlightProvider ;; 高亮当前 symbol。
;;:inlayHintProvider ;; 显示 inlay hint 提示。
))
;; 加强高亮的 symbol 效果。
;;(set-face-attribute 'eglot-highlight-symbol-face nil :background "#b3d7ff")
;; t: true, false: :json-false(不是 nil)。
;; gopls 配置参数: https://github.com/golang/tools/blob/master/gopls/doc/settings.setq
(setq-default eglot-workspace-configuration
'((:gopls . ((staticcheck . t)
(usePlaceholders . :json-false)
;; gopls 默认设置 GOPROXY=Off, 可能会导致 package 缺失进
;; 而引起补全异常. 开启 allowImplicitNetworkAccess 后将
;; 关闭 GOPROXY=Off.
;;(allowImplicitNetworkAccess . t)
)))))
consult-eglot 提供 consult-eglot-symbols
函数,方便选择 workspace 中的 symbol:
(use-package consult-eglot
:after (eglot consult))
下载 emacs-lsp-booster 可执行程序,然后使用 emacs-lsp-booster 来加速 eglot 的响应性能:
(use-package eglot-booster
:vc (:url "https://github.com/jdtsmith/eglot-booster")
:after (eglot)
:config (eglot-booster-mode))
eldoc 在 minibuffer echo-area 或 eldoc buffer 中显示文档和函数签名信息。
(use-package eldoc
:after (eglot)
:bind
(:map eglot-mode-map ("C-c C-d" . eldoc))
:config
(setq eldoc-idle-delay 0.1)
;; 打开 eldoc-buffer 时关闭 echo-area 显示, eldoc-buffer 会跟随显示 hover 信息, 如
;; 函数签名。
(setq eldoc-echo-area-prefer-doc-buffer t)
;; 在屏幕右侧显示 eldoc-buffer
(add-to-list 'display-buffer-alist
'("^\\*eldoc.*\\*"
(display-buffer-reuse-window display-buffer-in-side-window)
(dedicated . t)
(side . right)
(inhibit-same-window . t)))
;; 将 minibuffer 窗口高度设为 1,可以确保只显示一行(默认为小数,表示 frame 高度占
;; 比,会导致显示多行)。
(setq max-mini-window-height 1)
;; 为 nil 时只单行显示 eldoc 信息.
(setq eldoc-echo-area-use-multiline-p nil)
;; 一键显示和关闭 eldoc buffer。
(global-set-key (kbd "M-`")
(lambda()
(interactive)
(if (get-buffer-window "*eldoc*")
(delete-window (get-buffer-window "*eldoc*"))
(display-buffer "*eldoc*")))))
注:eglot 不给 eldoc 提供在 echo-area 显示的结构化成员或函数签名信息, 但是可以在 M-x
eldoc-doc-buffer(C-h-.)
打开的 eldoc buffer 中会显示这些信息。
eldoc-box 在 frame 右上角或光标位置显示 eldoc-doc-buffer 的内容。
(use-package eldoc-box
:after (eglot eldoc)
:bind
(:map eglot-mode-map
("C-M-k" . (lambda () (interactive) (eldoc-box-scroll-down 1)))
("C-M-j" . (lambda () (interactive) (eldoc-box-scroll-up 1)))
;; 按需弹出 posframe 来显示 eldoc buffer 内容。
("C-c C-d" . eldoc-box-help-at-point)
)
:config
(setq eldoc-box-max-pixel-height 600)
(setq eldoc-box-max-pixel-width 1200)
;; C-g 关闭弹出的 child frame。
(setq eldoc-box-clear-with-C-g t)
;; 在右上角显示 eldoc 帮助;
;;(add-hook 'eglot-managed-mode-hook #'eldoc-box-hover-mode t)
;; 在光标位置显示 eldoc 帮助;
;;(add-hook 'eglot-managed-mode-hook #'eldoc-box-hover-at-point-mode t)
)
brew install python
目前(2024.03.17)安装的是 python@12 版本,从该版本开始, 如果要 pip
安装 python 包, 必须安装到用户自己的 venv 环境, 否则报错(error:
externally-managed-environment)。具体参考: https://docs.brew.sh/Homebrew-and-Python
$ brew reinstall python
$ brew unlink [email protected] && brew link [email protected]
# 查看安装的位置
$ ls -l $(brew --prefix python)/libexec/bin
$ pip3 install pygments
error: externally-managed-environment
创建一个 ~/.venv
python 虚拟环境, 然后将 pip 包安装到该环境中:
zj@a:~$ python3 -m venv .venv
zj@a:~$ source ~/.venv/bin/activate
# 安装相关的包到虚拟环境中
(.venv) zj@a:~$ pip3 install pygments jinji2 ipython markdown flake8 yapf pyright grip debugpy
# 将 /Users/alizj/.venv/bin 添加到 PATH 中,这样后续不需要每次手动 active
# 更新 ~/.bashrc 中的 PATH: PATH=/Users/alizj/.venv/bin:$PATH
配置 Emacs 使用内置的 python-ts-mode 和 venv 虚拟环境:
;; 将 ~/.venv/bin 添加到 PATH 环境变量和 exec-path 变量中。
(setq my-venv-path "/Users/alizj/.venv/bin")
(setenv "PATH" (concat my-venv-path ":" (getenv "PATH")))
(setq exec-path (cons my-venv-path exec-path))
;; 指定 python.el 使用虚拟环境目录。
(setq python-shell-virtualenv-root "/Users/alizj/.venv")
(defun my/python-setup-shell (&rest args)
(if (executable-find "ipython3")
(progn
;; 使用 ipython3 作为 python shell.
(setq python-shell-interpreter "ipython3")
(setq python-shell-interpreter-args "--simple-prompt -i --InteractiveShell.display_page=True"))
(progn
;; 查找 python-shell-virtualenv-root 中的解释器.
(setq python-shell-interpreter "python3")
(setq python-interpreter "python3")
(setq python-shell-interpreter-args "-i"))))
;; 使用内置 python mode 和 LSP 来格式化代码(不适用 yapfify)
(use-package python
:init
;;(setq python-indent-guess-indent-offset t)
;;(setq python-indent-guess-indent-offset-verbose nil)
;;(setq python-indent-offset 2)
:hook
(python-mode . (lambda ()
(my/python-setup-shell))))
Python LSP Server 使用 basedpyright,它是微软 VSCode 使用的 pyright 和 pyglance 的开 源 fork 版本。
安装 basepyright:
which basedpyright || pip install basedpyright
配置 eglot 使用 basedpyright:
(add-to-list 'eglot-server-programs
'((python-mode python-ts-mode)
"basedpyright-langserver" "--stdio"))
pyright 不使用 pyenv .python-version
指定的 python 版本或 venv 来搜索依赖的 module,
而是使用=pyrightconfig.json= 文件中配置的 venv 和 venvPath:
- venvPath:指定查找 venv 目录的上级目录,可以包含多个 venv 环境;
- venv:指定 venvPath 目录下的、使用的虚拟环境名称, pyright 在该 venv 中搜索依赖的 package;
安装 pyenv-pyright
插件来方便的创建和更新 pyrightconfig.json
文件:
git clone https://github.com/alefpereira/pyenv-pyright.git $(pyenv root)/plugins/pyenv-pyright
使用方法:
- 使用
pyenv local
为项目指定pyenv virtualenv
; - 使用
pyenv pyright
来自动配置pyrightconfig.json
使用上一步指定的 virtualenv;
pyright 假设源文件位于项目 scr 目录下,但实际可能会在多个其它子目录(甚至嵌套情况)中
放置项目源码,即 multi-root
模式(对应于 vscode 中的多 worksapce 目录),这时可能出现
大量 import 错误,可以通过在项目根目录配置 pyrightconfig.json
文件来解决,例如(参考:
python module Import Resolution):
{
"venv": "venv-2.7.18",
"venvPath": "/Users/zhangjun/.pyenv/versions",
"verboseOutput": true,
"reportMissingTypeStubs": false,
"executionEnvironments": [
{
"root": "scripts",
"extraPaths": [
".", // scripts 目录下 py 文件导入同级 py 文件的情况
"scripts/appinstance_apply"
]
}
]
}
executionEnvironments:
- 列表中 root 指定各 workspace 的子目录,是有搜索优先级的,所以如果有相同路径前缀的 情况,应该从长到短依列出来:根据 python 文件的 from/import 语句来确定root 路径:即 从项目根目录(pyrightconfig.json 文件所在目录)开始到文件中导入路径最开始所在目录 之间的目录,都应该是 root。
- extraPaths 列表中的路径可以是绝对路径或相对路径(相对于 pyrightconfig.json 文件),
用于添加额外的 python module 搜索路径;
- 添加 “.” 是因为需要将 scripts 所在的目录也添加到 module 搜索路径,而不仅仅是 scripts 下的子目录;
- 官方的实例参考:Sample Config File 和 testState.test.ts;
pyright 不支持 python 2.x,如果在上面文件配置 "pythonVersion": "2.7"
则会报错。
安装最新 gopls 工具:
go install golang.org/x/tools/gopls@latest
使用 Emacs 内置的 go-ts-mode, 故不需要再单独安装 go-mode 包:
(require 'go-ts-mode)
;; go 使用 TAB 缩进。
(add-hook 'go-ts-mode-hook (lambda () (setq indent-tabs-mode t)))
设置 go 环境变量, eglot 启动 gopls 时传递它们:
(dolist (env '(("GOPATH" "/Users/alizj/go")
("GOPROXY" "https://goproxy.cn,https://goproxy.io,direct")
("GOPRIVATE" "*.alibaba-inc.com")
("GOOS" "linux")
("GOARCH" "arm64")))
(setenv (car env) (cadr env)))
查看本地和在线 go 文档:
(require 'go-ts-mode)
;; 查看光标处符号的本地文档.
(define-key go-ts-mode-map (kbd "C-c d .") #'godoc-at-point)
;; 查看 go std 文档。
(defun my/browser-gostd ()
(interactive)
(xwidget-webkit-browse-url "https://pkg.go.dev/std"))
(define-key go-ts-mode-map (kbd "C-c d s") 'my/browser-gostd)
;; 搜索 pkg.go.dev 在线 web 文档。
(defun my/browser-pkggo (query)
(interactive "ssearch: ")
(xwidget-webkit-browse-url
(concat "https://pkg.go.dev/search?q=" (string-replace " " "%20" query)) t))
(define-key go-ts-mode-map (kbd "C-c d w") 'my/browser-pkggo) ;; 助记: w -> web
安装或更新工具:
;; (setq gofmt-command "golangci-lint")
;; (setq gofmt-args "run --config /Users/alizj/.golangci.yml --fix")
(defvar go--tools '("golang.org/x/tools/gopls"
"github.com/rogpeppe/godef"
"golang.org/x/tools/cmd/goimports"
"honnef.co/go/tools/cmd/staticcheck"
"github.com/go-delve/delve/cmd/dlv"
"github.com/zmb3/gogetdoc"
"github.com/josharian/impl"
"github.com/cweill/gotests/..."
"github.com/fatih/gomodifytags"
"github.com/golangci/golangci-lint/cmd/golangci-lint"
"github.com/davidrjenni/reftools/cmd/fillstruct"))
(defun go-update-tools ()
(interactive)
(unless (executable-find "go")
(user-error "Unable to find `go' in `exec-path'!"))
(message "Installing go tools...")
(dolist (pkg go--tools)
(set-process-sentinel
(start-process "go-tools" "*Go Tools*" "go" "install" "-v" "-x" (concat pkg "@latest"))
(lambda (proc _)))))
(use-package go-fill-struct)
(use-package go-impl)
;; 自动为 struct field 添加 json tag。
(use-package go-tag
:init
(setq go-tag-args (list "-transform" "camelcase"))
:config
(require 'go-ts-mode)
(define-key go-ts-mode-map (kbd "C-c t a") #'go-tag-add)
(define-key go-ts-mode-map (kbd "C-c t r") #'go-tag-remove))
(use-package go-playground
:commands (go-playground-mode)
:config
(setq go-playground-init-command "go mod init"))
调试:
- 如果一个 git 项目下有多个 go module, 则需要在上层目录创建 workspace, 并将各 module
加入其中,否则可能出现 package import 失败的情况:
go work init go work use ./path/to/module1 ./path/to/module2
- 如果补全或自动提示异常, 执行
M-x eglot-events-buffer
看是否有报错(例如 GOPROXY=Off 导致的问题.)
使用 rustup 安装和管理 rust 工具链:
# 清理旧环境
mv ~/.cargo{,.bak}
brew uninstall rust rust-analyzer
brew install rustup-init
echo 'export PATH=$HOME/.cargo/bin:$PATH' >>~/.bashrc
rustup-init # 下载安装 rust stable 工具链
rustup component add rust-analyzer # 安装 rust lsp server
rustup component add clippy # rust lints
rustup component add rustfmt # rust formater
rustup component add rust-src
rustup component add rust-docs # 添加 rust 标准库文档
rustup toolchain list # 查看已安装的工具链
# 安装 nightly 工具链
rustup toolchain install nightly
# 设置缺省工具链为 nightly
rustup default nightly
# cargo 也支持在命令行上指定 toolchain 版本
cargo +nightly expand
查看文档:
rustup component add rust-docs # 添加 rust 标准库文档
rustup doc # 查看标准库文档
rustup doc topic # 查看某个 topic 的帮助文档,如 core,fn,std:char 等。
cargo doc --open # 查看当前项目和依赖的文档
清理没有使用的 crate:
cargo install cargo-sweep
cargo sweep --time 30
cargo sweep --installed
其它 cargo 插件:
cargo install cargo-watch
# 示例:先 cargo check,再 cargo test,最后 cargo run
cargo watch -x check -x test -x run
# 提供 cargo add 命令
cargo install cargo-edit
cargo add actix-web --vers 4.0.0
# 提供 cargo expand 宏展开命令
cargo install cargo-expand
# 提供 cargo +nightly udeps 命令,打印当前项目未使用的 crate
cargo install cargo-udeps
# 提供 cargo info 命令,查看 crate 的 features 和 version 信息
cargo install cargo-information
将 Rust 工具链目录添加到 PATH 环境变量和 Emacs 变量 exec-path 中:
(setq my-cargo-path "/Users/alizj/.cargo/bin")
(setenv "PATH" (concat my-cargo-path ":" (getenv "PATH")))
(setq exec-path (cons my-cargo-path exec-path))
;; https://github.com/mozilla/sccache?tab=readme-ov-file
;; cargo install sccache --locked
;;(setenv "RUSTC_WRAPPER" "/Users/alizj/.cargo/bin/sccache")
;; brew install sccache
(setenv "RUSTC_WRAPPER" "/opt/homebrew/bin/sccache")
配置 rust-mode:
;; https://github.com/jwiegley/dot-emacs/blob/master/init.org#rust-mode
(use-package rust-mode
:after (eglot)
:init
(require 'rust-ts-mode)
;; rust-mode 作为 rust-ts-mode 而非 prog-mode 的子 mode。
(setq rust-mode-treesitter-derive t)
:config
;; rust-analyzer 使用 rustfmt 来格式化代码
;;(setq rust-format-on-save t)
(setq rust-rustfmt-switches '("--edition" "2021"))
;; treesit-auto 默认不将 XX-mode-hook 添加到对应的 XX-ts-mode-hook 上, 需要手动指定。
(setq rust-ts-mode-hook rust-mode-hook)
;; rust 建议使用空格而非 TAB 来缩进.
(add-hook 'rust-ts-mode-hook (lambda () (setq indent-tabs-mode nil)))
;; 参数列表参考:https://rust-analyzer.github.io/manual.html#configuration
(add-to-list
'eglot-server-programs
'((rust-ts-mode rust-mode) .
("rust-analyzer"
:initializationOptions
(
:rustfmt
(
:extraArgs ["+nightly"]
)
;; 20240910 不能关闭 checkOnSave,否则 flymake diagnose 可能不生效。
;;:checkOnSave :json-false
;;:cachePriming (:enable :json-false) ;; 启动时不预热缓存.
:check
(
:command "clippy"
;;https://esp-rs.github.io/book/tooling/visual-studio-code.html#using-rust-analyzer-with-no_std
:allTargets :json-false
;; 不发送 --workspace 给 cargo check, 只检查当前 package.
;; 20240910 可能导致基于 workspace 的 标准库 lsp 不生效,故不能设置。
;;:workspace :json-false
)
;;:procMacro (:attributes (:enable t) :enable :json-false)
:cargo
(
;;:buildScripts (:enable :json-false)
;;:extraArgs ["--offline"] ;; 不联网节省时间.
;;:features "all"
;;:noDefaultFeatures t
:cfgs (:tokio_unstable "")
;;:autoreload :json-false
)
:diagnostics
(
;;:enable :json-false
:disabled ["unresolved-proc-macro" "unresolved-macro-call"]
)
:inlayHints
(
:bindingModeHints (:enable t)
:closureCaptureHints (:enable t)
:closureReturnTypeHints (:enable t)
:lifetimeElisionHints (:enable t)
:expressionAdjustmentHints (:enable t)
)
;; :linkedProjects
;; [
;; "/Users/alizj/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/Cargo.toml",
;; "/Users/alizj/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/Cargo.toml",
;; "/Users/alizj/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/proc_macro/Cargo.toml",
;; "/Users/alizj/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/test/Cargo.toml"
;; ]
)))))
rust-analyzer 调用 rustfmt (rustup component add rustfmt) 来格式化代码,为代码添 加~#[rustfmt::skip]~ 可以忽略格式化。
创建如下全局 rustfmt 配置文件;
# 配置选项参考:
# https://rust-lang.github.io/rustfmt/?version=v1.7.1&search=#chain_width
edition = "2021"
# 确保方法链式调用时,每行一个方法调用,这样 inlayhint 会显示上一个方法的返回值类型。
chain_width = 0
# 是否格式化为单行函数。
fn_single_line = true
# 最大行长度,超过后自动折行。
max_width = 80
# 函数各参数单独一行
fn_args_layout = "Vertical"
# 以下是 unstable features,只能在 nightly channel 使用。
wrap_comments = true
normalize_comments = true
format_code_in_doc_comments = true
comment_width = 80
format_strings = true
imports_granularity = "Crate"
enum_discrim_align_threshold = 20
rust-playground 快速测试环境:
- BUGFIX: https://github.com/grafov/rust-playground/pull/11/files
- 设置 Cargo.toml 模板文件变量中 edition 值为 2021(默认是 2018)。
(use-package rust-playground
:config
(setq rust-playground-cargo-toml-template
"[package]
name = \"foo\"
version = \"0.1.0\"
authors = [\"opsnull <[email protected]>\"]
edition = \"2021\"
[dependencies]"))
eglot-x 为 Rust 提供了几个好用的命令:
M-x eglot-x-reload-workspace
:在 Cargo.toml 文件发生变化时手动执行而不需要重启 eglot;M-x eglot-x-expand-macro
:展开宏定义(或者使用 cargo expand 命令来显示宏展开后的 定义);M-x eglot-x-open-external-documentation
:用浏览器查看光标处的 rust 文档;
(use-package eglot-x
:after (eglot rust-mode)
:vc (:url "https://github.com/nemethf/eglot-x")
:init
(require 'rust-ts-mode) ;; 绑定 rust-ts-mode-map 需要。
:config
(eglot-x-setup))
查看本地和在线文档:
(with-eval-after-load 'rust-ts-mode
;; 使用 xwidget 打开光标处 symbol 的本地 crate 文档,由于是 web 网页,链接和类型都
;; 可以点击。
(define-key rust-ts-mode-map (kbd "C-c d .") #'eglot-x-open-external-documentation)
;; 查看本地 rust std 文档;
(defun my/browser-ruststd ()
(interactive)
(xwidget-webkit-browse-url "file:///Users/alizj/.rustup/toolchains/stable-aarch64-apple-darwin/share/doc/rust/html/std/index.html" t))
(define-key rust-ts-mode-map (kbd "C-c d s") 'my/browser-ruststd)
;; 在线 https:://docs.rs/ 搜索文档.
(defun my/browser-docsrs (query)
(interactive "ssearch: ")
(xwidget-webkit-browse-url
(concat "https://docs.rs/releases/search?query=" (string-replace " " "%20" query)) t))
(define-key rust-ts-mode-map (kbd "C-c d w") 'my/browser-docsrs) ;; 助记: w -> web
;; 在线搜索 crate 包。
(defun my/search-crates.io (query)
(interactive "ssearch: ")
(xwidget-webkit-browse-url
(concat "https://crates.io/search?q=" (string-replace " " "%20" query)) t))
(global-set-key (kbd "C-c d c") 'my/browser-docsrs) ;; 助记: c -> crates.io
)
cargo package 不再维护, 故切换到 cargo-mode package, 它提供了 Cargo.toml 管理命令。
- C-c a e(cargo-execute-task) :列出所有支持的 cargo task,如 build/test 等,同时可以 添加和删除依赖(需要指定 PREFIX 命令来输入依赖名称)。
(use-package cargo-mode
:after (rust-mode)
:custom
;; cargo-mode 缺省为 compilation buffer 使用 comint mode, 设置为 nil 使用
;; compilation。
(cargo-mode-use-comint nil)
:hook
(rust-ts-mode . cargo-minor-mode)
:config
;; 自动滚动显示 compilation buffer 内容。
(setq compilation-scroll-output t))
其他技巧:
- 创建一个 struct 对象时, 可以使用 eglot code-action 来自动填充对象成员;
- Cargo.toml 文件发生变化时, rust-analyzer 不会自动更新处理, 需要重启 eglot 才能自动
补全新的crate. 两个解决办法:
- 使用 eglot-x 中的 M-x eglot-x-reload-workspace 命令;
- 或者先将
所有依赖
提前添加到 Cargo.toml 文件, 然后再启动 eglot;
安装依赖工具:
brew install multimarkdown
pip3 install grip
multimarkdown 将 markdown 转换为 html 进行 preview,可以结合 xwidget webkit 或 grip 进行实时预览:
(use-package markdown-mode
:commands (markdown-mode gfm-mode)
:mode
(("README\\.md\\'" . gfm-mode)
("\\.md\\'" . markdown-mode)
("\\.markdown\\'" . markdown-mode))
:init
(when (executable-find "multimarkdown")
(setq markdown-command "multimarkdown"))
(setq markdown-enable-wiki-links t)
(setq markdown-italic-underscore t)
(setq markdown-asymmetric-header t)
(setq markdown-make-gfm-checkboxes-buttons t)
(setq markdown-gfm-uppercase-checkbox t)
(setq markdown-fontify-code-blocks-natively t)
(setq markdown-gfm-additional-languages "Mermaid")
(setq markdown-content-type "application/xhtml+xml")
(setq markdown-css-paths
'("https://cdn.jsdelivr.net/npm/github-markdown-css/github-markdown.min.css"
"https://cdn.jsdelivr.net/gh/highlightjs/cdn-release/build/styles/github.min.css"))
(setq markdown-xhtml-header-content "
<meta name='viewport' content='width=device-width, initial-scale=1, shrink-to-fit=no'>
<style>
body {
box-sizing: border-box;
max-width: 740px;
width: 100%;
margin: 40px auto;
padding: 0 10px;
}
</style>
<link rel='stylesheet' href='https://cdn.jsdelivr.net/gh/highlightjs/cdn-release/build/styles/default.min.css'>
<script src='https://cdn.jsdelivr.net/gh/highlightjs/cdn-release/build/highlight.min.js'></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
document.body.classList.add('markdown-body');
document.querySelectorAll('pre code').forEach((code) => {
if (code.className != 'mermaid') {
hljs.highlightBlock(code);
}
});
});
</script>
<script src='https://unpkg.com/[email protected]/dist/mermaid.min.js'></script>
<script>
mermaid.initialize({
theme: 'default', // default, forest, dark, neutral
startOnLoad: true
});
</script>
"))
使用 grip 来预览 markdown 文件,它调用 github markdown API 来渲染文件,从而确保渲染后
风格和 Github一致。为了避免 API 调用频率限制,可以创建一个空 scop 的 Access Token,然
后将 username 和 token 保存到 ~/.authinfo.gpg
文件中:
machine api.github.com login [email protected] password YOUR_TOKEN
在 Markdown Buffer 中,执行 M-x grip-mode
来启用实时预览,然后可以执行如下命令:
- M-x grip-start-preview
- M-x grip-stop-preview
- M-x grip-restart-preview
- M-x grip-browse-preview
(use-package grip-mode
:defer
:after (markdown-mode)
:config
(setq grip-preview-use-webkit nil)
(setq grip-preview-host "127.0.0.1")
;; 保存文件时才更新预览。
(setq grip-update-after-change nil)
;; 从 ~/.authinfo 文件获取认证信息。
(require 'auth-source)
(let ((credential (auth-source-user-and-password "api.github.com")))
(setq grip-github-user (car credential)
grip-github-password (cadr credential)))
(define-key markdown-mode-command-map (kbd "g") #'grip-mode))
为 markdown 文件添加目录:
(use-package markdown-toc
:after(markdown-mode)
:config
(define-key markdown-mode-command-map (kbd "r") #'markdown-toc-generate-or-refresh-toc))
使用 emacs 内置的 yaml-ts-mode。
安装 yaml 语言服务器:
which yaml-language-server || npm install -g yaml-language-server
yaml 格式说明:
- 不使用 TAB 而使用空格缩进;
- 对于多行字符串,使用 name: | 格式,后续第一行缩进必须大于 name: 所在行,而且以后续 以第一行缩进为准来删除后续各行前面的空白,所以各行的缩进必须大于等于第一行的缩进, 超过的部分空白得以保留;
Emacs 使用 bash-ts-mode
来编辑 shell 脚本。
安装 bash language server:
npm i -g bash-language-server
bash language server 使用 shellcheck 工具来做语法检查和静态分析:
brew install shellcheck
设置脚本缩进规则:
(setq sh-basic-offset 4)
(setq sh-indentation 4)
参考:
安装 llvm/clang/clang-format 包:
brew install llvm lld clang-format
export LDFLAGS="-L/opt/homebrew/opt/llvm/lib"
export CPPFLAGS="-I/opt/homebrew/opt/llvm/include"
export PATH="/opt/homebrew/opt/llvm/bin:$PATH"
# 打印格式化配置参数
clang-format --dump-config
将 llvm bin 目录添加到 emacs:
(setq my-llvm-path "/opt/homebrew/opt/llvm/bin")
(setenv "PATH" (concat my-llvm-path ":" (getenv "PATH")))
(setq exec-path (cons my-llvm-path exec-path))
创建全局 ~/.clang-format 文件,也可以在各 project root 目录创建项目配置文件,主要配置 的是:
- Tab 和 Indent 缩进;
- 不对头文件进行排序,防止编译报错;
# clang-format configuration file. Intended for clang-format >= 11.
#
# For more information, see:
#
# Documentation/process/clang-format.rst
# https://clang.llvm.org/docs/ClangFormat.html
# https://clang.llvm.org/docs/ClangFormatStyleOptions.html
# linux 内核开发风格:
# https://raw.githubusercontent.com/torvalds/linux/master/.clang-format
---
# 基本配置
DisableFormat: false
Language: Cpp # 可以是 'Cpp', 'Java', 'JavaScript', 'Proto', 'TableGen' 等等
BasedOnStyle: WebKit # 基于 WebKit 风格,因为它最接近 Linux 内核的风格
# 缩进
IndentWidth: 8 # 缩进宽度
TabWidth: 8 # 制表符宽度
UseTab: ForIndentation # 使用制表符进行缩进,空格用于对齐
# 换行
AllowShortIfStatementsOnASingleLine: false # 禁止 if 语句在单行内
AllowShortLoopsOnASingleLine: false # 禁止循环语句在单行内
AlwaysBreakBeforeMultilineStrings: true # 多行字符串之前总是换行
BreakBeforeBraces: Linux # 使用 Linux 风格的大括号位置
ColumnLimit: 100 # 每行的字符限制
# 空格
SpaceBeforeParens: ControlStatements # 在控制语句的括号前加空格(if, for, while, 等)
SpaceAfterCStyleCast: true # C 风格的强制类型转换后加空格
# 注释
CommentPragmas: '^ IWYU pragma:' # 支持 IWYU pragmas 注释
IndentExternBlock: AfterExternBlock # 在 extern "C" 块后进行缩进
# 包含文件
SortIncludes: false # 禁用头文件排序,防止编译出错。
IncludeBlocks: Preserve # 保持头文件分组
# 函数定义
AlignTrailingComments: true # 尾部注释对齐
AlignConsecutiveDeclarations: true # 连续的声明对齐
# 其他
NamespaceIndentation: All # 所有命名空间内的代码都缩进
ReflowComments: true # 重新格式化注释
# 特定于 C 语言的设置
Standard: Cpp11 # 使用 C++11 标准
(use-package tempel
:bind
(("M-+" . tempel-complete)
("M-*" . tempel-insert))
:init
;; 自定义模板文件。
(setq tempel-path "/Users/alizj/emacs/templates")
(add-hook 'conf-mode-hook 'tempel-setup-capf)
(add-hook 'prog-mode-hook 'tempel-setup-capf)
(add-hook 'text-mode-hook 'tempel-setup-capf)
(defun tempel-setup-capf ()
(setq-local completion-at-point-functions (cons #'tempel-expand completion-at-point-functions)))
;; 确保 tempel-setup-capf 位于 eglot-managed-mode-hook 前,这样 corfu 才会显示
;; tempel 的自动补全。
;; https://github.com/minad/tempel/issues/103#issuecomment-1543510550
(add-hook #'eglot-managed-mode-hook 'tempel-setup-capf))
(use-package tempel-collection)
;; https://gitlab.com/skybert/my-little-friends/-/blob/master/emacs/.emacs#L295
(setq compilation-ask-about-save nil
compilation-always-kill t
compilation-scroll-output 'first-error ;; 滚动显示到第一个出错位置。
compilation-context-lines 10
compilation-skip-threshold 2
;;compilation-window-height 100
)
(define-key compilation-mode-map (kbd "q") 'delete-window)
;; 显示 shell 转义字符的颜色。
(add-hook 'compilation-filter-hook
(lambda ()
(ansi-color-apply-on-region (point-min) (point-max))))
;; 编译结束且失败时自动切换到 compilation buffer。
(setq compilation-finish-functions
(lambda (buf str)
(if (null (string-match ".*exited abnormally.*" str))
;; 没有错误, 什么也不做。
nil
;; 有错误时切换到 compilation buffer。
(switch-to-buffer-other-window buf)
(end-of-buffer))))
citre 是基于 Ctags(Universal Ctags 版本)的代码浏览器工具,也支持集成使用 GNU global 的 GTAGS 文件。
安装 GNU global 和 pygments, global 依赖并自动安装 universal-ctags, 通过 pygments 能 生成更丰富的TAG 内容,同时支持 reference 搜索。
- https://github.com/universal-ctags/citre/blob/master/docs/user-manual/citre-global.md
- global 默认使用 brew 安装的 [email protected] 和 pygments, 而不能直接使用 pip install pygments.
brew install global pygments # 提供 global、gtags 命令, gtags 使用 pygments 支持多语言
# 在 ~/.bashrc 中添加如下配置:
# 统一的 tags 文件目录
export GTAGSOBJDIRPREFIX=~/.cache/gtags/
mkdir $GTAGSOBJDIRPREFIX
export GTAGSCONF=/opt/homebrew/opt/global/share/gtags/gtags.conf
# 使用 pygments 支持更多的语言,支持 reference 搜索。
export GTAGSLABEL=pygments
创建和更新 GNU global GTAGS 文件(保存到 GTAGSOBJDIRPREFIX 环境变量指定的位置,如 ~/.cache/gtags/):
- M-x citre-global-create-database
- M-x citre-global-update-database
zj@a:~/.cache/gtags/Users/alizj/go/src/**/bp-agent$ ls -l
total 2.8M
-rw-r--r-- 1 alizj 32K 3 30 11:53 GPATH
-rw-r--r-- 1 alizj 600K 3 30 11:53 GRTAGS # reference tags
-rw-r--r-- 1 alizj 1.9M 3 30 11:53 GTAGS # tags
注意:以下两个命令创建 Universal Ctags 的 ctags 文件(项目有 .tags/ 目录或 .tags 或 tags 文件),而非 GNU global GTAGS 文件,不支持 references,故不建议使用:
- M-x citre-create-tags-file
- M-x citre-update-tags-file
如果误使用了上面的命令创建 ctags 文件则后续使用 xref-find-references 会 hang,需要删 除。
对于开启了 citre-mode 的 buffer,citre 向 xref-backend-functions 中添加 citre-xref-backend, 而且位于列表的开始,这样 xref 先使用 citre-xref-backend,当无返回 结果时再查找其它注册的 xref backend 如eglot-xref-backend 等。
- xref-backend-functions 可能会被添加多个 backend,但 xref 使用第一个返回非空数据的 backend,而忽 略后续 backend。
新版本的 citre-xref-backend 支持自动集成 eglot,它先使用 eglot 的结果,如果为空,再使 用 tags 文件的结果作为后备,所以不管项目是否存在 tags 文件,都可以为所有 prog-mode 开 启 citre-mode。
其他 xref 特性,如 imenu/xref-find-references/xref-find-definitions 等都会使用 citre 提供的输入。同时 xref 和 consult 结合,可以使用 consult 来预览 xref 的结果。
配置 citre:
(setenv "GTAGSOBJDIRPREFIX" (expand-file-name "~/.cache/gtags/"))
(setenv "GTAGSCONF" (car (file-expand-wildcards "/opt/homebrew/opt/global/share/gtags/gtags.conf")))
(setenv "GTAGSLABEL" "pygments")
(use-package citre
:after (eglot)
:config
;; 只使用支持 reference 的 GNU Global tags。
(setq citre-completion-backends '(global))
(setq citre-find-definition-backends '(global))
(setq citre-find-reference-backends '(global))
(setq citre-tags-in-buffer-backends '(global))
(setq citre-auto-enable-citre-mode-backends '(global))
(setq citre-use-project-root-when-creating-tags t)
(setq citre-peek-file-content-height 20)
;; 打开列表中的 major mode 文件且项目具有 global tags 文件时,才自动开启 citre。
(setq citre-auto-enable-citre-mode-modes
'(
c-mode
c-ts-mode
rust-mode
rust-ts-mode
;; go-mode
;; go-ts-mode
))
;; 使用 eglot-managed-mode-hook 而非 find-file-hook,从而确保 citre-mode 在 eglot
;; 启动后才开启。
;; 执行 citre-auto-enable-citre-mode 而非 citre-mode 命令:
;; 1. 前者会检查 citre-auto-enable-citre-mode-modes 变量中的 major mode 和项目是否
;; 有 global tags文件,只有两者均满足时,才开启 citre。
;; 2. 后者是不管 major mode 类型和是否有 tags 文件,均开启 citre。
(add-hook 'eglot-managed-mode-hook #'citre-auto-enable-citre-mode)
(define-key citre-mode-map (kbd "s-.") 'citre-jump)
(define-key citre-mode-map (kbd "s-,") 'citre-jump-back)
(define-key citre-mode-map (kbd "s-?") 'citre-peek-reference)
(define-key citre-mode-map (kbd "s-p") 'citre-peek)
(define-key citre-peek-keymap (kbd "s-n") 'citre-peek-next-line)
(define-key citre-peek-keymap (kbd "s-p") 'citre-peek-prev-line)
(define-key citre-peek-keymap (kbd "s-N") 'citre-peek-next-tag)
(define-key citre-peek-keymap (kbd "s-P") 'citre-peek-prev-tag))
在 ~/.authinfo.gpg 中添加 api.openai.com key,然后使用本地 socks5h 代理访问 API。
- azure 各 region 的访问速度测试:https://www.azurespeed.com/Azure/Latency
(use-package gptel
:ensure t
:config
(setq
gptel-default-mode 'org-mode
gptel-model 'gpt-4o
gptel-backend
(gptel-make-azure "Azure"
:protocol "https"
:host "westus3ai.openai.azure.com"
:endpoint "/openai/deployments/4fouro/chat/completions?api-version=2024-02-15-preview"
:stream t
:key #'gptel-api-key
:models '(gpt-4o))))
安装 vterm 依赖:
brew install cmake libtool exiftran
配置 vterm:
(use-package vterm
:hook
(vterm-mode . (lambda ()
;; 关闭一些 mode,提升显示性能。
(setf truncate-lines nil)
(setq-local show-paren-mode nil)
(setq-local global-hl-line-mode nil)
(display-line-numbers-mode -1) ;; 不显示行号。
;;; vterm buffer 使用 fixed pitch 的 mono 字体,否则部分终端表格之
;;; 类的程序会对不齐。
(set (make-local-variable 'buffer-face-mode-face) 'fixed-pitch)
(buffer-face-mode t)))
:config
(setq vterm-set-bold-hightbright t)
(setq vterm-always-compile-module t)
(setq vterm-max-scrollback 100000)
(setq vterm-timer-delay 0.01) ;; nil: no delay
(add-to-list 'vterm-tramp-shells '("ssh" "/bin/bash"))
;; vterm buffer 名称,%s 为 shell 的 PROMPT_COMMAND 变量的输出。
(setq vterm-buffer-name-string "*vt: %s")
;; 使用 M-y(consult-yank-pop) 粘贴剪贴板历史中的内容。
(define-key vterm-mode-map [remap consult-yank-pop] #'vterm-yank-pop)
(define-key vterm-mode-map (kbd "C-l") nil)
;; 防止输入法切换冲突。
(define-key vterm-mode-map (kbd "C-\\") nil))
(use-package multi-vterm
:after (vterm)
:config
(define-key vterm-mode-map [(control return)] #'multi-vterm))
vterm-toggle:
(use-package vterm-toggle
:after (vterm)
:custom
;; 由于 TRAMP 模式下关闭了 projectile,scope 不能设置为 'project。
;;(vterm-toggle-scope 'dedicated)
(vterm-toggle-scope 'project)
:config
(global-set-key (kbd "C-`") 'vterm-toggle)
(global-set-key (kbd "C-M-`") 'vterm-toggle-cd)
(define-key vterm-mode-map (kbd "M-RET") #'vterm-toggle-insert-cd)
;; 切换到空闲的 vterm buffer 并插入一个 cd 命令,或者创建一个新的 vterm buffer。
(define-key vterm-mode-map (kbd "M-i") 'vterm-toggle-cd-show)
(define-key vterm-mode-map (kbd "M-n") 'vterm-toggle-forward)
(define-key vterm-mode-map (kbd "M-p") 'vterm-toggle-backward)
(define-key vterm-copy-mode-map (kbd "M-i") 'vterm-toggle-cd-show)
(define-key vterm-copy-mode-map (kbd "M-n") 'vterm-toggle-forward)
(define-key vterm-copy-mode-map (kbd "M-p") 'vterm-toggle-backward))
vterm-extra 提供了 vterm buffer 命令行编辑的能力,结束后按 C-c C-c 自动粘贴到对应的 vterm 中:
(use-package vterm-extra
:vc (:url "https://github.com/Sbozzolo/vterm-extra")
:config
(define-key vterm-mode-map (kbd "C-c C-e") #'vterm-extra-edit-command-in-new-buffer))
eshell:
(setq eshell-history-size 300)
(setq explicit-shell-file-name "/bin/bash")
(setq shell-file-name "/bin/bash")
(setq shell-command-prompt-show-cwd t)
(setq explicit-bash-args '("--noediting" "--login" "-i"))
;; 提示符只读
(setq comint-prompt-read-only t)
;; 命令补全
(setq shell-command-completion-mode t)
;; 高亮模式
(autoload 'ansi-color-for-comint-mode-on "ansi-color" nil t)
(add-hook 'shell-mode-hook 'ansi-color-for-comint-mode-on t)
(setenv "SHELL" shell-file-name)
(setenv "ESHELL" "bash")
(add-hook 'comint-output-filter-functions 'comint-strip-ctrl-m)
;; 在当前 frame 下方打开或关闭 eshell buffer。
(defun startup-eshell ()
"Fire up an eshell buffer or open the previous one"
(interactive)
(if (get-buffer-window "*eshell*<42>")
(delete-window (get-buffer-window "*eshell*<42>"))
(progn
(eshell 42))))
(global-set-key (kbd "s-`") 'startup-eshell)
(add-to-list 'display-buffer-alist
'("\\*eshell\\*<42>"
(display-buffer-below-selected display-buffer-at-bottom)
(inhibit-same-window . t)
(window-height . 0.33)))
;; eshell history 使用 consult-history。
(load-library "em-hist.el")
(keymap-set eshell-hist-mode-map "C-s" #'consult-history)
(keymap-set eshell-hist-mode-map "C-r" #'consult-history)
;; 重置 M-r/s 快捷键,这样 consult-line 等可用。
(define-key eshell-hist-mode-map (kbd "M-r") nil)
(define-key eshell-hist-mode-map (kbd "M-s") nil)
使用 GNU 系列替换 MacOS 自带的 BSD 风格的 coreutils 包:
which tac || brew install coreutils
which trash || brew install trash
;; 避免 undo-more: No further undo information 报错.
;; 10X bump of the undo limits to avoid issues with premature.
;; Emacs GC which truncages the undo history very aggresively
(setq undo-limit 800000)
(setq undo-strong-limit 12000000)
(setq undo-outer-limit 120000000)
(global-auto-revert-mode 1)
(setq revert-without-query (list "\\.png$" "\\.svg$")
auto-revert-verbose nil)
(setq global-mark-ring-max 600)
(setq mark-ring-max 600)
(setq kill-ring-max 600)
(use-package emacs
:init
;; 粘贴于光标处, 而不是鼠标指针处。
(setq mouse-yank-at-point t)
(setq initial-major-mode 'fundamental-mode)
;; 按中文折行。
(setq word-wrap-by-category t)
;; 退出自动杀掉进程。
(setq confirm-kill-processes nil)
(setq use-short-answers t)
(setq confirm-kill-emacs #'y-or-n-p)
(setq ring-bell-function 'ignore)
;; 不显示行号, 否则鼠标会飘。
(add-hook 'artist-mode-hook (lambda () (display-line-numbers-mode -1)))
;; bookmark 发生变化时自动保存(默认是 Emacs 正常退出时保存)。
(setq bookmark-save-flag 1)
;; 不创建 lock 文件。
(setq create-lockfiles nil)
;; 启动 Server 。
(unless (and (fboundp 'server-running-p)
(server-running-p))
(server-start)))
(use-package hydra :commands defhydra)
历史记录:
(use-package recentf
:config
(setq recentf-save-file "~/.emacs.d/recentf")
;; 自动清理 recentf 记录(无效的、重复的、被 exclude 的等),防止已经删除的文件继续
;; 出现在 consult-buffer 列表中
(setq recentf-auto-cleanup 'mode)
;; 每 5min 以及 emacs 退出时保存 recentf-list。
;; 20241017: 配置这两个参数后,recentf 将被清空。
;;(run-at-time nil (* 5 60) 'recentf-save-list)
;;(add-hook 'kill-emacs-hook #'recentf-save-list)
(setq recentf-max-menu-items 100)
(setq recentf-max-saved-items 100)
;; recentf-exclude 的参数是正则表达式列表,不支持 ~ 引用家目录。
;;; emacs-dashboard 不显示这里排除的文件。
(setq recentf-exclude
`(
,(recentf-expand-file-name "~/.emacs.d/\\(straight\\|ln-cache\\|etc\\|var\\|.cache\\|backup\\|elfeed\\)/.*")
,(recentf-expand-file-name "~/.emacs.d/\\(recentf\\|bookmarks\\|archived.org\\)")
,(recentf-expand-file-name "~/go/mod/.*")
;; 不在 recentf 中记录 tramp 文件,防止 tramp 扫描时卡住。
,tramp-file-name-regexp
"^/tmp"
"\\.bak\\'"
"\\.gpg\\'"
"\\.gz\\'"
"\\.tgz\\'"
"\\.xz\\'"
"\\.zip\\'"
"^/ssh:"
"\\.png\\'"
"\\.jpg\\'"
"/\\.git/"
"\\.gitignore\\'"
"\\.log\\'"
"COMMIT_EDITMSG"
"\\.pyi\\'"
"\\.pyc\\'"
"/private/var/.*"
"^/usr/local/Cellar/.*"
".*/vendor/.*"
".*/target/.*"
"/Applications/.*"
,(concat package-user-dir "/.*-autoloads\\.egl\\'")))
(recentf-mode 1))
dired:
;; dired
(setq my-coreutils-path "/opt/homebrew/opt/coreutils/libexec/gnubin")
(setenv "PATH" (concat my-coreutils-path ":" (getenv "PATH")))
(setq exec-path (cons my-coreutils-path exec-path))
(use-package emacs
:config
(setq dired-dwim-target t)
;; @see
;; https://emacs.stackexchange.com/questions/5649/sort-file-names-numbered-in-dired/5650#5650
;; 下面的参数只对安装了 coreutils (brew install coreutils) 的包有效,否则会报错。
(setq dired-listing-switches "-laGh1v --group-directories-first"))
(use-package diredfl :config (diredfl-global-mode))
搜索 grep/isearch:
(use-package grep
:config
(setq grep-highlight-matches t)
(setq grep-find-ignored-directories
(append (list ".git" ".cache" "vendor" "node_modules" "target")
grep-find-ignored-directories))
(setq grep-find-ignored-files
(append (list "*.blob" "*.gz" "TAGS" "projectile.cache" "GPATH" "GRTAGS" "GTAGS" "TAGS" ".project" )
grep-find-ignored-files)))
(global-set-key "\C-cn" 'find-dired)
(global-set-key "\C-cN" 'grep-find)
(setq isearch-allow-scroll 'unlimited)
;; 显示当前和总的数量。
(setq isearch-lazy-count t)
(setq isearch-lazy-highlight t)
diff/ediff:
;; diff
(use-package diff-mode
:init
(setq diff-default-read-only t)
(setq diff-advance-after-apply-hunk t)
(setq diff-update-on-the-fly t))
(use-package ediff
:config
(setq ediff-keep-variants nil)
(setq ediff-split-window-function 'split-window-horizontally)
;; 不创建新的 frame 来显示 Control-Panel。
(setq ediff-window-setup-function #'ediff-setup-windows-plain))
剪贴板和字符编码:
;; 使用系统剪贴板,实现与其它程序相互粘贴。
(setq x-select-enable-clipboard t)
(setq select-enable-clipboard t)
(setq x-select-enable-primary t)
(setq select-enable-primary t)
;; UTF8 字符。
(prefer-coding-system 'utf-8)
(setq locale-coding-system 'utf-8
default-buffer-file-coding-system 'utf-8)
(set-buffer-file-coding-system 'utf-8)
(set-language-environment "UTF-8")
(setq-default buffer-file-coding-system 'utf8)
(set-default-coding-systems 'utf-8)
(setenv "LC_ALL" "zh_CN.UTF-8")
buffer/file:
(use-package ibuffer
:config
(setq ibuffer-expert t)
(setq ibuffer-use-other-window nil)
(setq ibuffer-movement-cycle nil)
(setq ibuffer-default-sorting-mode 'recency)
(setq ibuffer-use-header-line t)
(add-hook 'ibuffer-mode-hook #'hl-line-mode)
(global-set-key (kbd "C-x C-b") #'ibuffer))
;; 保存 Buffer 时自动更新 #+LASTMOD: 时间戳。
(setq time-stamp-start "#\\+\\(LASTMOD\\|lastmod\\):[ \t]*")
(setq time-stamp-end "$")
(setq time-stamp-format "%Y-%m-%dT%02H:%02m:%02S%5z")
;; #+LASTMOD: 必须位于文件开头的 line-limit 行内, 否则自动更新不生效。
(setq time-stamp-line-limit 30)
(add-hook 'before-save-hook 'time-stamp t)
;; 以下自定义函数参考自:https://github.com/jiacai2050/dotfiles/blob/master/.config/emacs/i-edit.el
(defun my/json-format ()
(interactive)
(save-excursion
(if mark-active
(json-pretty-print (mark) (point))
(json-pretty-print-buffer))))
(defun my/delete-file-and-buffer (buffername)
"Delete the file visited by the buffer named BUFFERNAME."
(interactive "bDelete file")
(let* ((buffer (get-buffer buffername))
(filename (buffer-file-name buffer)))
(when filename
(delete-file filename)
(message "Deleted file %s" filename)
(kill-buffer))))
(defun my/diff-buffer-with-file ()
"Compare the current modified buffer with the saved version."
(interactive)
(let ((diff-switches "-u")) ;; unified diff
(diff-buffer-with-file (current-buffer))
(other-window 1)))
(defun my/copy-current-filename-to-clipboard ()
"Copy `buffer-file-name' to system clipboard."
(interactive)
(let ((filename (if-let (f buffer-file-name)
f
default-directory)))
(if filename
(progn
(message (format "Copying %s to clipboard..." filename))
(kill-new filename))
(message "Not a file..."))))
;; https://gitlab.com/skybert/my-little-friends/-/blob/2022-emacs-from-scratch/emacs/.emacs
;; Rename current buffer, as well as doing the related version control commands to
;; rename the file.
(defun my/rename-this-buffer-and-file ()
"Renames current buffer and file it is visiting."
(interactive)
(let ((filename (buffer-file-name)))
(if (not (and filename (file-exists-p filename)))
(message "Buffer is not visiting a file!")
(let ((new-name (read-file-name "New name: " filename)))
(cond
((vc-backend filename) (vc-rename-file filename new-name))
(t
(rename-file filename new-name t)
(rename-buffer new-name)
(set-visited-file-name new-name)
(set-buffer-modified-p nil)
(message
"File '%s' successfully renamed to '%s'"
filename
(file-name-nondirectory new-name))))))))
(global-set-key (kbd "C-x C-r") 'my/rename-this-buffer-and-file)
C-a/C-e 移动到行或代码的开头、结尾:
(use-package mwim
:config
(define-key global-map [remap move-beginning-of-line] #'mwim-beginning-of-code-or-line)
(define-key global-map [remap move-end-of-line] #'mwim-end-of-code-or-line))
增量扩展选择的区域:
(use-package expand-region
:config
(global-set-key (kbd "C-=") #'er/expand-region))
自动备份:
(defvar backup-dir (expand-file-name "~/.emacs.d/backup/"))
(if (not (file-exists-p backup-dir))
(make-directory backup-dir t))
;; 文件第一次保存时备份。
(setq make-backup-files t)
(setq backup-by-copying t)
;; 不备份 tramp 文件,其它文件都保存到 backup-dir, https://stackoverflow.com/a/22077775
(setq backup-directory-alist `((,tramp-file-name-regexp . nil) (".*" . ,backup-dir)))
;; 备份文件时使用版本号。
(setq version-control t)
;; 删除过多的版本。
(setq delete-old-versions t)
(setq kept-new-versions 6)
(setq kept-old-versions 2)
;; 不备份版本控制的文件.
(setq vc-make-backup-files nil)
(defvar autosave-dir (expand-file-name "~/.emacs.d/autosave/"))
(if (not (file-exists-p autosave-dir))
(make-directory autosave-dir t))
;; auto-save 访问的文件。
(setq auto-save-default t)
(setq auto-save-list-file-prefix autosave-dir)
(setq auto-save-file-name-transforms `((".*" ,autosave-dir t)))
(setq kill-buffer-delete-auto-save-files t)
(setq auto-save-include-big-deletions t)
Emacs 30 xwidget-webkit 对 Mac 支持不好( Better support for xwidget-webkit), 部分功能只有 GTK/X11才 支持, 如: buffer 内搜索 increase-search/webkit-history:
- 如果要复制 xwidget 的内容,需要选择后右击,从上下文菜单中选择 copy。
- 如果 window 窗口较小,可以按 a 来自动调整(对应 xwidget-webkit-adjust-size-dispatch 命令)。
(setq url-user-agent
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36")
(setq xwidget-webkit-buffer-name-format "*webkit* [%T] - %U")
(setq xwidget-webkit-enable-plugins t)
(setq browse-url-firefox-program "/Applications/Firefox.app/Contents/MacOS/firefox")
;; browse-url-firefox, browse-url-default-macosx-browser
(setq browse-url-browser-function 'xwidget-webkit-browse-url)
(setq xwidget-webkit-cookie-file "~/.emacs.d/cookie.txt")
(add-hook 'xwidget-webkit-mode-hook
(lambda ()
;;(setq kill-buffer-query-functions nil)
(setq header-line-format nil)
(display-line-numbers-mode 0)
;;(local-set-key "q" (lambda () (interactive) (kill-this-buffer)))
(local-set-key (kbd "C-t") (lambda () (interactive) (xwidget-webkit-browse-url "https://google.com" t)))))
(defun my/browser-open-at-point (url)
(interactive
(list (let ((url (thing-at-point 'url)))
(if (equal major-mode 'xwidget-webkit-mode)
(read-string "url: " (xwidget-webkit-uri (xwidget-webkit-current-session)))
(read-string "url: " url)))))
(xwidget-webkit-browse-url url t))
(defun my/browser-search (query)
(interactive "ssearch: ")
(xwidget-webkit-browse-url
(concat "https://duckduckgo.com?q=" (string-replace " " "%20" query)) t))
(define-prefix-command 'my-browser-prefix)
(global-set-key (kbd "C-c o") 'my-browser-prefix)
(define-key my-browser-prefix (kbd "o") 'my/browser-open-at-point)
(define-key my-browser-prefix (kbd "s") 'my/browser-search)
;; https://github.com/syl20bnr/spacemacs/issues/6587#issuecomment-232890021
;; make these keys behave like normal browser
(require 'xwidget)
(define-key xwidget-webkit-mode-map [mouse-4] 'xwidget-webkit-scroll-down)
(define-key xwidget-webkit-mode-map [mouse-5] 'xwidget-webkit-scroll-up)
(define-key xwidget-webkit-mode-map (kbd "<up>") 'xwidget-webkit-scroll-down)
(define-key xwidget-webkit-mode-map (kbd "<down>") 'xwidget-webkit-scroll-up)
(define-key xwidget-webkit-mode-map (kbd "M-w") 'xwidget-webkit-copy-selection-as-kill)
(define-key xwidget-webkit-mode-map (kbd "C-c") 'xwidget-webkit-copy-selection-as-kill)
;; 自动调整 xwidget-webkit 窗口大小(也可以手动按 a 来调整)。
(add-hook 'window-configuration-change-hook
(lambda ()
(when (equal major-mode 'xwidget-webkit-mode)
(xwidget-webkit-adjust-size-dispatch))))
;; make xwidget default browser
(setq browse-url-browser-function
(lambda (url session)
(other-window 1)
(xwidget-webkit-browse-url url)))
在线文档和翻译:
;;在线搜索, 先选中 region 再执行搜索。
(use-package engine-mode
:config
(engine/set-keymap-prefix (kbd "C-c s"))
(engine-mode t)
;;(setq engine/browser-function 'eww-browse-url)
(setq engine/browser-function 'xwidget-webkit-browse-url)
(defengine github "https://github.com/search?ref=simplesearch&q=%s" :keybinding "h")
(defengine google "https://google.com/search?q=%s" :keybinding "g"))
;; Google 翻译
(use-package google-translate
:config
;; C-n/p 切换翻译类型。
(setq google-translate-translation-directions-alist
'(("en" . "zh-CN") ("zh-CN" . "en")))
(global-set-key (kbd "C-c d t") #'google-translate-smooth-translate))
MacOS 互操作:
- osx-trash 不支持 TRAMP 删除远程文件,解决办法:用 %m 标记文件,然后按 ! 执行 rm 命 令。
- 在 finder 中打开当前文件或目录:M-! 后执行命令:
open .
;; 删除文件时, 将文件移动到回收站。
(use-package osx-trash
:config
(when (eq system-type 'darwin)
(osx-trash-setup))
(setq-default delete-by-moving-to-trash t))
;; 在 Finder 中打开当前文件。
(use-package reveal-in-osx-finder
:commands (reveal-in-osx-finder))
帮助增强:
;; 在帮助文档底部显示 lisp demo.
(use-package elisp-demos
:config
(advice-add 'describe-function-1 :after #'elisp-demos-advice-describe-function-1)
(advice-add 'helpful-update :after #'elisp-demos-advice-helpful-update))
;; 相比 Emacs 内置 Help, 提供更多上下文信息。
(use-package helpful
:config
(global-set-key (kbd "C-h f") #'helpful-callable)
(global-set-key (kbd "C-h v") #'helpful-variable)
(global-set-key (kbd "C-h k") #'helpful-key)
(global-set-key (kbd "C-c C-d") #'helpful-at-point)
(global-set-key (kbd "C-h F") #'helpful-function)
(global-set-key (kbd "C-h C") #'helpful-command))
安装依赖:
brew install poppler automake mupdf
配置 pdf-tools:
(use-package pdf-tools
;; :ensure-system-package
;; ((pdfinfo . poppler)
;; (automake . automake)
;; (mutool . mupdf)
;; ("/usr/local/opt/zlib" . zlib))
:init
;; 使用 scaling 确保中文字体不模糊
(setq pdf-view-use-scaling t)
(setq pdf-view-use-imagemagick nil)
(setq pdf-annot-activate-created-annotations t)
(setq pdf-view-resize-factor 1.1)
(setq-default pdf-view-display-size 'fit-page)
(setq pdf-annot-activate-created-annotations t)
:hook
((pdf-view-mode . pdf-view-themed-minor-mode)
(pdf-view-mode . pdf-view-auto-slice-minor-mode)
(pdf-view-mode . pdf-isearch-minor-mode))
:config
(define-key pdf-view-mode-map (kbd "C-s") 'isearch-forward)
;;(add-hook 'pdf-view-mode-hook (lambda() (linum-mode -1)))
(setq pdf-info-epdfinfo-program "/opt/homebrew/bin/epdfinfo")
(setenv "PKG_CONFIG_PATH" "/opt/homebrew/opt/zlib/lib/pkgconfig:/opt/homebrew/opt/pkgconfig:/opt/homebrew/lib/pkgconfig")
(pdf-tools-install))
;; pdf 转为 png 时使用更高分辨率(默认 90)。
(setq doc-view-resolution 144)
;;(use-package org-noter)
- pdf-tools 默认是白底黑字,可以:
- 深色模式:
M-x pdf-view-midnight-minor-mode
- 主题模式:
M-x pdf-view-themed-minor-mode
- 深色模式:
- 搜索中文时,需要使用系统中文输入法和 isearch 模式, 或者使用
M-s o(occur)
;
本配置参考了以下仓库代码: