Skip to content

opsnull/emacs

Repository files navigation

My Emacs Dotfile

Emacs 是 Hacker 的一种生活方式,而这是符合我口味的私人定制。

更多 Emacs 内容请访问我的博客

static/images/2024-09-03_10-55-30_screenshot.png

编译安装

从源码编译、安装最新 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 "geekard@qq.com")

;; 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 代理, 这里安装支持 socks5GNU curl 版本:

brew install curl
export PATH="/opt/homebrew/opt/curl/bin:$PATH"

GNU curl 添加到 EmacsPATH 环境变量和 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 打开文件(如 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)

;; 像素平滑滚动。
(pixel-scroll-precision-mode t)
(setq fast-but-imprecise-scrolling t)
(setq scroll-conservatively 10
      scroll-margin 0
      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-0" . 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 缩放,在偶数字号的情况下可以实现中英 文等宽等高。

常用字体命令:

  • 查看 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 输入法后端引擎 librimeemacs-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 # 自定义词频文件
sed -i -e 's/custom_phrase.txt/opsnull_custom_phrase/g' opsnull_custom_phrase.txt # 修改其中的 db_name

rime_ice 拼音方案调整(如模糊音,动态词频,自定义词语文件等):

  • 自定义短语:向自定义短语词典文件 opsnull_custom_phrase.txt 添加自定义短语,custom_prase/db_class 为 stabledb,是只读的,不会动态调频。(可以设置为 tabledb 来动态调频)。
  • 首次添加该文件后需要执行 M-x rime-deployM-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-prime-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"))))

补全和预览

本部分使用的三方包说明:

  1. vertico:minibuffer 补全;
  2. corfu:光标处补全;
  3. orderless:提供 flex/regex 过滤风格;
  4. consult:实时预览和高级过滤;
  5. embark:为 minibuffer/buffer 选中的内容提供快捷操作;
  6. 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 using orderless-without-literal, that is, both !bad and bad! will match strings that do not contain the substring bad.
  • % : makes the string match ignoring diacritics and similar inflections on characters (it uses the function char-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 
          (1052843656 (styles . (orderless basic))) ;;使用 M-SPC 来分隔光标处的多个筛选条件。
          (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))

org-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 110)
  (add-hook 'org-mode-hook 'olivetti-mode))

;; fill-column 值要小于 olivetti-body-width 才能正常折行。
(setq-default fill-column 98)
;; 由于 auto-fill 可能会打乱代码的字符串和注释,故为 prog-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:nilnum: 2 是否生效;
(my-latex "#+DATE: " (format-time-string "%Y-%m-%d %a") n 
	  "#+SUBTITLE: 内部资料,注意保密!
#+AUTHOR: 张俊(zj@opsnull.com)
# 中文语言环境(目录等用中文显示)。
#+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: (\"geekard@qq.com\") -*-")

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,而是使用 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

project 使用 top-down 方式来检查项目路径中是否存在 .project 文件,所以在上层各路径的目录中不应该存 在 .project 文件,否则会导致判断失败。

  1. 手动标记项目根目录:在目录下创建 .project 文件
  2. 查看当前项目的 project root: (project-current)
  3. 手动添加 project 目录: M-x project-remember-projects-under
  4. 调试目录的 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)

magit

(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

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 u") 'treesit-fold-open)
  (global-set-key (kbd "C-c f F") 'treesit-fold-open-recursively)
  (global-set-key (kbd "C-c f U") 'treesit-fold-close-all)
  (global-set-key (kbd "C-c f O") 'treesit-fold-open-all)
  (global-set-key (kbd "C-c f t") 'treesit-fold-toggle))

flymake

flymake 为当前 buffer 提供错误检查/诊断功能,它在如下情况检查(诊断)buffer 是否有错误,错误信息直 接显示在 buffer 区域, 同时也会发送给 eldoc:

  1. 执行 M-x flymake-start;
  2. 超过 flymake-no-changes-timeout(默认 0.5) ,设置为 nil 后表示无限长;
  3. 保存 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))

eglot

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 ()
    (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)

  (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.md
  (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

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) (scroll-eldoc-box-frame ?u)))
        ("C-M-j" . (lambda () (interactive) (scroll-eldoc-box-frame ?d)))
	("C-c C-d" . eldoc-box-help-at-point) ;; 按需弹出 posframe 来显示 eldoc buffer 内容。
	)

  :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-doc* child frame 中的内容。
  (defun scroll-eldoc-box-frame (direction)
    (interactive "cScroll direction (u for up, d for down): ")
    (let ((frame (get-buffer-window " *eldoc-box*" t)))
      (when frame
	(with-selected-window frame
          (cond ((eq direction ?u) (scroll-down-line))
		((eq direction ?d) (scroll-up-line)))))))

  ;; 在右上角显示 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)
  )  

python

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 python@3.12 && brew link python@3.12
# 查看安装的位置
$ 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

使用方法:

  1. 使用 pyenv local 为项目指定 pyenv virtualenv;
  2. 使用 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:

  1. 列表中 root 指定各 workspace 的子目录,是有搜索优先级的,所以如果有相同路径前缀的情况,应该从长 到短依列出来:根据 python 文件的 from/import 语句来确定root 路径:即从项目根目录 (pyrightconfig.json 文件所在目录)开始到文件中导入路径最开始所在目录之间的目录,都应该是 root。
  2. extraPaths 列表中的路径可以是绝对路径或相对路径(相对于 pyrightconfig.json 文件),用于添加额外 的 python module 搜索路径;
    • 添加 “.” 是因为需要将 scripts 所在的目录也添加到 module 搜索路径,而不仅仅是 scripts 下的子目录;
  3. 官方的实例参考:Sample Config FiletestState.test.ts

pyright 不支持 python 2.x,如果在上面文件配置 "pythonVersion": "2.7" 则会报错。

go

安装最新 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"))

调试:

  1. 如果一个 git 项目下有多个 go module, 则需要在上层目录创建 workspace, 并将各 module 加入其中,否 则可能出现 package import 失败的情况:
    go work init
    go work use ./path/to/module1 ./path/to/module2
        
  2. 如果补全或自动提示异常, 执行 M-x eglot-events-buffer 看是否有报错(例如 GOPROXY=Off 导致的问题.)

rust

使用 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

将 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")

配置 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
      (
       ;; 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 快速测试环境:

  1. BUGFIX: https://github.com/grafov/rust-playground/pull/11/files
  2. 设置 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 <geekard@qq.com>\"]
edition = \"2021\"

[dependencies]"))

eglot-x 为 Rust 提供了几个好用的命令:

  1. M-x eglot-x-reload-workspace :在 Cargo.toml 文件发生变化时手动执行而不需要重启 eglot;
  2. M-x eglot-x-expand-macro :展开宏定义(或者使用 cargo expand 命令来显示宏展开后的定义);
  3. 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))

其他技巧:

  1. 创建一个 struct 对象时, 可以使用 eglot code-action 来自动填充对象成员;
  2. Cargo.toml 文件发生变化时, rust-analyzer 不会自动更新处理, 需要重启 eglot 才能自动补全新的 crate. 两个解决办法:
    1. 使用 eglot-x 中的 M-x eglot-x-reload-workspace 命令;
    2. 或者先将 所有依赖 提前添加到 Cargo.toml 文件, 然后再启动 eglot;

markdown

安装依赖工具:

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/mermaid@8.4.8/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 geekard@qq.com 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))

shell

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)

参考:

  1. Google Shell Style Guide

clang

安装最新的 llvm 和 clang:

brew install llvm
export LDFLAGS="-L/opt/homebrew/opt/llvm/lib"
export CPPFLAGS="-I/opt/homebrew/opt/llvm/include"
export PATH="/opt/homebrew/opt/llvm/bin:$PATH"

将 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))

clangd 是 clang 的 language server,它使用(非 clang 编译器)项目根目录下的下面 3 个配置文件:

  1. .clang-format: 全局或项目级别的配置,指定 clangd 格式化选项的;
  2. compile_commands.json: 全局或项目级别配置文件,指定该项目下各目录文件的编译方式;
  3. compile_flags.txt: 全局或项目级别配置文件,用于为项目指定缺省的编译选项。如果存在则 clangd 会 忽略 compile_commands.json 文件。

安装 clang-format 工具,为 clangd 生成配置文件:

brew install clang-format
clang-format --dump-config

创建全局 ~/.clang-format 文件,也可以在各 project root 目录创建项目配置文件,主要配置的是:

  1. Tab 和 Indent 缩进;
  2. 不对头文件进行排序,防止编译报错;
# 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 标准

在各 project root 目录中创建:

  1. compile_commands.json 文件 :为不同的文件单独指定编译参数;
  2. compile_flags.txt 文件:为项目中所有文件来指定 build flags 。

为了便于 clangd lsp 能准确对项目源码进行生成 compile_commands.json 文件的方式:

  1. Cmake 项目:
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=1
  1. 其它项目,用 bear:
apt-get install bear
make clean; bear -- make 

在 project root 目录下创建 compile_flags.txt 文件,文件中的每行为一个 build flags,然后 clang 用如下 形式来使用它们:

clang $FLAGS some_file.cc # $FLAGS 变量内容来源于 compile_flags.txt 文件

compile_flags.txt 示例(如果 flag 的 args 中间有空格,需要分两行);

  • 相对路径是相对于 project root 目录。
  • -c 是必须的,用来对每个源文件单独编译,否则表示生成可执行程序。
-target
bpf
-D__TARGET_ARCH_x86
-Wall
-Wextra
-I.
-I../headers
-idirafter
/Users/zhangjun/codes/ubuntu-5.15.0-75-headers
-g
-O2
-c

tempel

(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)

compilation

;; 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))))

;; 在 frame 右侧显示两个上下布局的 dedicated window,分别用于显示 eldoc 和 compilation 窗口。
(defun my/setup-windows ()
  (interactive)
  (let ((eldoc-buffer (get-buffer-create "*eldoc*"))
        (compilation-buffer (get-buffer-create "*compilation*")))
    (set-window-dedicated-p 
     (display-buffer-in-side-window eldoc-buffer '((side . right) (slot . 0))) t)
    (set-window-dedicated-p 
     (display-buffer-in-side-window compilation-buffer '((side . right) (slot . 1))) t)))
(global-set-key (kbd "C-c d d") 'my/setup-windows)

chatgpt-shell

在 ~/.authinfo.gpg 文件中添加 api.openai.com 的 key,然后使用本地 socks5h 代理访问 API。

(use-package shell-maker)
(use-package ob-chatgpt-shell :defer t)
(use-package ob-dall-e-shell :defer t)
(use-package chatgpt-shell
  :requires shell-maker
  :defer t
  :config
  (setq chatgpt-shell-model-version "gpt-4o")
  (setq chatgpt-shell-model-temperature 0.7)
  (setq chatgpt-shell-request-timeout 300)
  (setq chatgpt-shell-highlight-blocks t)
  (setq chatgpt-shell-insert-queries-inline t)
  (setq chatgpt-shell-chatgpt-streaming t)
  (setq chatgpt-shell-system-prompt 2) ;; 默认使用 Programming prompt

  (setq chatgpt-shell-system-prompts
    `(("tl;dr" . "Be as succint but informative as possible and respond in tl;dr form to my queries")
      ("General" . "You use org-mode format to structure responses. Always show code snippets in org-mode source code block format.")
      ;; Based on https://github.com/benjamin-asdf/dotfiles/blob/8fd18ff6bd2a1ed2379e53e26282f01dcc397e44/mememacs/.emacs-mememacs.d/init.el#L768
      ("Programming" . ,(chatgpt-shell--append-system-info
			 "The user is a programmer with very limited time.
                        You treat their time as precious. You do not repeat obvious things, including their query.
                        You are as concise as possible in responses.
                        You never apologize for confusions because it would waste their time.
                        You use emacs org-mode format to structure responses.
                        Always show code snippets in org-mode source code block format.
                        Don't explain code snippets.
                        Whenever you output updated code for the user, only show diffs, instead of entire snippets."))
      ("Positive Programming" . ,(chatgpt-shell--append-system-info
                                  "Your goal is to help the user become an amazing computer programmer.
                        You are positive and encouraging.
                        You love see them learn.
                        You do not repeat obvious things, including their query.
                        You are as concise in responses. You always guide the user go one level deeper and help them see patterns.
                        You never apologize for confusions because it would waste their time.
                        You use markdown liberally to structure responses. Always show code snippets in markdown blocks with language labels.
                        Don't explain code snippets. Whenever you output updated code for the user, only show diffs, instead of entire snippets."))))

  (require 'ob-chatgpt-shell)
  (ob-chatgpt-shell-setup)
  (require 'ob-dall-e-shell)
  (ob-dall-e-shell-setup)
  ;;(setq chatgpt-shell-api-url-base "http://127.0.0.1:1090")
  (setq chatgpt-shell-api-url-base "https://westus3ai.openai.azure.com")
  (setq chatgpt-shell-api-url-path  "/openai/deployments/gpt4oai/chat/completions?api-version=2024-02-15-preview")
  (setq chatgpt-shell-openai-key (auth-source-pick-first-password :host "westus3ai.openai.azure.com"))
  ;; azure 使用 api-key 而非 openai 的 Authorization: Bearer 认证头部。
  (setq chatgpt-shell-auth-header 
	(lambda ()
	  (format "api-key: %s" (auth-source-pick-first-password :host "westus3ai.openai.azure.com"))))
  )

终端

安装 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)
  ;; emacs 退出时清理 recentf 记录。
  (add-hook 'kill-emacs-hook #'recentf-cleanup)

  ;; 每 5min 以及 emacs 退出时保存 recentf-list。
  ;;(run-at-time nil (* 5 60) 'recentf-save-list)
  ;;(add-hook 'kill-emacs-hook #'recentf-save-list)

  (setq recentf-max-menu-items 50)
  (setq recentf-max-saved-items 100) ;; default 20

  ;; 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/.*")
          ,tramp-file-name-regexp ;; 不在 recentf 中记录 tramp 文件,防止 tramp 扫描时卡住。
          "^/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))

pdf

安装依赖:

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)

refs

本配置参考了以下仓库代码:

  1. seagle0128/.emacs.d
  2. protesilaos/dotfiles
  3. bbatsov/prelude
  4. MatthewZMD/.emacs.d
  5. condy0919/.emacs.d
  6. manateelazycat/lazycat-emacs
  7. jiacai2050/dotfiles
  8. natecox/dotfiles
  9. daviwil
  10. skybert/my-little-friends
  11. casouri/lunarymacs

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published