Skip to main content

My Emacs Dotfile

··22801 字·
emacs emacs
Table of Contents
emacs - 这篇文章属于一个系列。
§ 1: 本文

更多 Emacs 内容请访问我的博客: https://blog.opsnull.com/emacs/

1 install
#

编译安装最新 Emacs 29:

brew uninstall emacs-plus@29
brew install emacs-plus@29  --with-no-frame-refocus --with-xwidgets --with-imagemagick --with-poll --with-dragon-icon --with-native-comp --with-poll --HEAD
brew unlink emacs-plus@29 && brew link emacs-plus@29
ln -sf /opt/homebrew/opt/emacs-plus/Emacs.app /Applications/

2 init
#

early-init.el 启动时最开始执行的文件,执行复杂逻辑可能会失败,所以该文件尽量以变量定义为主。

(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/13/"))
  (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)
  )

;; 加载较新的 .el 文件。
(setq-default load-prefer-newer t)
(setq-default lexical-binding t)
(setq lexical-binding t)

在单独文件保存自定义配置,避免污染 ~/.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))))

设置 Emacs 搜索二进制工具的路径, Emacs 查找外部程序时使用 exec-path 而非 PATH 变量:

(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"
                    ))
;; 设置 Emacs 启动外部程序时(如 lsp server)给它们传入的环境变量。
(mapc (lambda (p)
        (setenv "PATH" (concat p ":" (getenv "PATH"))))
      my-bin-path)
(let ((paths my-bin-path))
  (dolist (path paths)
    (setq exec-path (cons path exec-path))))

3 package
#

设置各种软件包源:

  • M-x use-package-report : 查看 package 加载时间(按 S 排序)。
(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 package-install-upgrade-built-in t)

vc-use-packageuse-package 增加 :vc 指令,从而可以从 github 等安装软件包。

(unless (package-installed-p 'vc-use-package)
  (package-vc-install "https://github.com/slotThe/vc-use-package"))

4 proxy
#

Mac 自带的 curl 不支持 socks5 代理, 这里安装支持 socks5 代理的 GNU curl 版本:

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

将安装的 curl 添加到 PATH 环境变量和 exec-path 变量中:

(setq my-coreutils-path "/opt/homebrew/opt/curl/bin/")
(setenv "PATH" (concat my-coreutils-path ":" (getenv "PATH")))
(setq exec-path (cons my-coreutils-path  exec-path))

全局 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 '("0.0.0.0" "127.0.0.1" "localhost" "10.0.0.0/8" "172.0.0.0/8"
                    ".cn" ".alibaba-inc.com" ".taobao.com" ".antfin-inc.com"
                    ".openai.azure.com" ".baidu.com" ".aliyun-inc.com"))

;; Google 默认会 403 缺少 UA 的请求。
(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 (:fetcher github :repo 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 超过 1024Bytes 时发送 Expect: 100-continue 导致 1s 延迟.
                                      "-H" "Expect: ''"
                                      ;;"-u" ,github-auth
                                      "--user-agent" ,my/user-agent
                                      ))))
(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))
  ;; curl/wget/ruby/python/go 都感知 no_proxy 变量: https://superuser.com/a/1690537
  (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))

(defun proxy-socks-disable ()
  (interactive)
  (require 'socks)
  (setq url-gateway-method 'native
        socks-noproxy nil)
  (setenv "all_proxy" "")
  (setenv "ALL_PROXY" ""))

(proxy-socks-enable)

5 tuning
#

设置 epa,用于 GPG 加解密:

(use-package epa
  :config
  ;; gpg 私钥使用这里定义的 user 信息。
  (setq user-full-name "zhangjun")
  (setq user-mail-address "[email protected]")
  (setq auth-sources '("~/.authinfo.gpg"))
  (setq auth-source-cache-expiry 300)
  ;;(setq auth-source-debug t)

  (setq-default
   ;; 缺省使用 email 地址加密。
   epa-file-select-keys nil
   epa-file-encrypt-to user-mail-address
   ;; 使用 minibuffer 输入 GPG 密码。
   epa-pinentry-mode 'loopback
   epa-file-cache-passphrase-for-symmetric-encryption t)
  (require 'epa-file)
  (epa-file-enable))

关闭容易误操作的按键。

(let ((keys '("s-w" "C-z" "<mouse-2>" "s-k" "s-o" "s-t" "s-p" "s-n" "s-," "s-."
              "s--" "s-0" "s-+" "C-<wheel-down>" "C-<wheel-up>")))
  (dolist (key keys)
    (global-unset-key (kbd key))))

Mac 按键调整:s- 表示 Super,S- 表示 Shift, H- 表示 Hyper。

;; command 作为 Meta 键。
(setq mac-command-modifier 'meta)
;; option 作为 Super 键。
(setq mac-option-modifier 'super)
;; fn 作为 Hyper 键。
(setq ns-function-modifier 'hyper)

提升 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)
(setq-default ad-redefinition-action 'accept)
(setq bidi-inhibit-bpa t)
(setq bidi-paragraph-direction 'left-to-right)
(setq-default bidi-display-reordering nil)

Garbage Collector Magic Hack, 提升 vterm buffer、json 文件响应性能。

(use-package gcmh
  :init
  ;;(setq garbage-collection-messages t)
  ;;(setq gcmh-verbose t)
  (setq gcmh-idle-delay 'auto) ;; default is 15s
  (setq gcmh-auto-idle-delay-factor 10)
  (setq gcmh-high-cons-threshold (* 32 1024 1024))
  (gcmh-mode 1)
  (gcmh-set-high-threshold))

(add-hook 'after-init-hook #'garbage-collect t)

6 ui
#

关闭 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))

不显示 Title Bar:

;; square corner: 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))

光标和行号:

;; 高亮当前行。
(global-hl-line-mode t)
(setq global-hl-line-sticky-flag t)

;; 显示行号。
(global-display-line-numbers-mode t)

;; 光标和字符宽度一致(如 TAB)
(setq x-stretch-cursor nil)

frame 设置:

;; 不在新 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)

在 frame 底部显示的窗口列表:

(setq display-buffer-alist
      `((,(rx bos (or
                   "*Apropos*"
                   "*Help*"
                   "*helpful"
                   "*info*"
                   "*Summary*"
                   "*vt"
                   "*lsp-bridge"
                   "*Org"
                   "*Google Translate*"
                   "*eldoc*"
                   " *eglot"
                   "*compilation*"
                   "Shell Command Output") (0+ not-newline))
         (display-buffer-below-selected display-buffer-at-bottom)
         (inhibit-same-window . t)
         (window-height . 0.33))))

启动后显示模式,加 t 参数让 togg-frame-XX 最后运行,这样最大化才生效:

;;(add-hook 'window-setup-hook 'toggle-frame-fullscreen t)
(add-hook 'window-setup-hook 'toggle-frame-maximized t)

透明背景:

(defun my/toggle-transparency ()
  (interactive)
  ;; 分别为 frame 获得焦点和失去焦点的不透明度。
  (set-frame-parameter (selected-frame) 'alpha '(90 . 90))
  (add-to-list 'default-frame-alist '(alpha . (90 . 90)))
  (add-to-list 'default-frame-alist '(alpha-background . 90)) ;; Emacs 29
  )

窗口调整:

;; 调整窗口大小。
(global-set-key (kbd "s-<left>") 'shrink-window-horizontally)
(global-set-key (kbd "s-<right>") 'enlarge-window-horizontally)
(global-set-key (kbd "s-<down>") 'shrink-window)
(global-set-key (kbd "s-<up>") 'enlarge-window)

;; 切换窗口。
(global-set-key (kbd "s-o") #'other-window)

滚动显示:

(global-set-key (kbd "s-j") (lambda () (interactive) (scroll-up 1)))
(global-set-key (kbd "s-k") (lambda () (interactive) (scroll-down 1)))

;; 像素平滑滚动。
(pixel-scroll-precision-mode t)

向下/向上翻另外的窗口。

(global-set-key (kbd "s-v") 'scroll-other-window)
(global-set-key (kbd "C-s-v") 'scroll-other-window-down)

org-mode buffer 内容居中显示:

  • 设置 olivetti body 宽度: C-c | (M-x olivetti-set-width)
  • olivetti-body-widthfill-column 都是 buffer local 变量,需要使用 setq-default 才能在所有 buffer 中生效。
(use-package olivetti
  :config
  ;; 内容区域宽度,超过后自动折行。
  (setq-default olivetti-body-width 120)
  (add-hook 'org-mode-hook 'olivetti-mode))
;; fill-column 值要小于 olivetti-body-width 才能正常折行。
(setq-default fill-column 100)

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)
  (setq dashboard-items '((recents . 15) (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 t)
  (doom-modeline-env-enable-go nil)
  (doom-modeline-buffer-file-name-style 'truncate-nil) ;; relative-from-project
  (doom-modeline-vcs-max-length 30)
  (doom-modeline-github nil)
  (doom-modeline-time-icon nil)
  :config
  (display-battery-mode 0)
  (column-number-mode t)
  (size-indication-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 display-time-day-and-date t)
  (setq indicate-buffer-boundaries (quote left)))

;; 为 vterm-mode 定义简化的 modeline,提升性能。
(doom-modeline-def-modeline 'my-term-modeline
  '(buffer-info) ;; 左侧
  '(misc-info minor-modes input-method)) ;; 右侧
(add-to-list 'doom-modeline-mode-alist '(vterm-mode . my-term-modeline))

字体:

  • 英文字体: Iosevka Comfy;
  • 中文字体:霞鹜文楷屏幕阅读版 LxgwWenKai-Screen,屏幕阅读版主要是对字体做了加粗,便于屏幕阅读;
  • 英文 Iosevka/Sarasa 字体和中文 LxgwWenKai 字体,按照 1:1 缩放,在偶数字号的情况下可以实现等宽等高;
(use-package fontaine
  :config
  (setq fontaine-latest-state-file
        (locate-user-emacs-file "fontaine-latest-state.eld"))

  (setq fontaine-presets
        '((small
           :default-family "Iosevka Comfy Motion"
           :default-height 80
           :variable-pitch-family "Iosevka Comfy Fixed")
          (regular) ;; 使用缺省配置。
          (medium
           :default-weight semilight
           :default-height 115
           :bold-weight extrabold)
          (large
           :inherit medium
           :default-height 150)
          (presentation
           :default-height 180)
          (t
           :default-family "Iosevka Comfy"
           :default-weight regular
           :default-height 160 ;; 默认字号, 需要是偶数才能实现等宽等高。
           :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)
  (define-key global-map (kbd "C-c f") #'fontaine-set-preset)
  (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)

常用命令:

  • 查看 Emacs 支持的字体名称: (print (font-family-list))
  • 安装、更新 Icon 字体: M-x all-the-icons-install-fonts
  • 查看光标处字体: M-x describe-char
  • 查看 emacs 支持的字体名称: (print (font-family-list));

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))
          (agenda-date . (semilight 1.5))
          (agenda-structure . (variable-pitch light 1.9))
          (t . (variable-pitch 1.1))))
  (setq ef-themes-region '(intense no-extend neutral)))

自动切换深浅主题:

  • light: zenburn ef-elea-light ef-spring ef-day doom-one-light
  • dark: sanityinc-tomorrow-eighties zenburn ef-elea-dark ef-night doom-palenight
(defun my/load-theme (appearance)
  (interactive)
  (pcase appearance
    ('light (load-theme 'ef-elea-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)))

pulsar:高亮光标移动到的行。

(use-package pulsar
  :config
  (setq pulsar-pulse t)
  (setq pulsar-delay 0.25)
  (setq pulsar-iterations 5)
  (setq pulsar-face 'pulsar-magenta)
  (setq pulsar-highlight-face 'pulsar-yellow)
  (pulsar-global-mode 1)
  (add-hook 'next-error-hook #'pulsar-pulse-line-red))

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-}" 'tab-bar-switch-to-next-tab)
  (keymap-global-set "s-{" 'tab-bar-switch-to-prev-tab)
  (keymap-global-set "s-w" 'tab-bar-close-tab)
  (global-set-key (kbd "s-0") 'tab-bar-close-tab)

  ;; 为 tab 添加序号,便于快速切换。
  ;; 参考:https://christiantietze.de/posts/2022/02/emacs-tab-bar-numbered-tabs/
  (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))

tar-bar 命令前缀:C-x t

t (other-tab-prefix)
在下一个新的 tab 中显示下一个 command 的 buffer;

C-r (find-file-read-only-other-tab) :

C-f (find-file-other-tab) :

f (find-file-other-tab) :

b (switch-to-buffer-other-tab) :

r (tab-rename)
重命名当前 tab 的名称,然后一直不会变。
d (dired-other-tab)
在新的 tab 中显示 dired 内容。

nyan:modeline 彩虹猫。

(use-package nyan-mode
  :config
  (setq nyan-animate-nyancat t)
  (setq nyan-wavy-trail t)
  (nyan-mode)
  (nyan-start-animation))

7 rime
#

安装 RIME 输入法后端引擎 librime

  • emacs-rime 直接和该引擎打交道,不需要安装 Mac 输入法前端 App 鼠须管 squirrel;
  • 通过 squirrel App 同步用户配置数据,可能会导致 userdb 数据损坏(~/Library/Rime/rime_ice.userdb/LOG 文件有日志记录),进而导致 RIME 动态词频、用户词典等功能异常。
wget https://github.com/rime/librime/releases/download/1.11.0/rime-76a0a16-macOS-universal.tar.bz2
tar -xvf rime-76a0a16-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 雾凇拼音输入法方案:

  • 雾凇拼音 主页有一些输入用例, 如果打同样的拼音可以补全相同的中文候选词就证明已经成功用上了雾凇拼音;
  • 以词定字:[: 上屏当前词句的第一个字,]: 上屏当前词句的最后一个字;
  • 中英文标点: 输入 vbd 后选择, v 开头有一系列快捷键;
  • 常见问题: https://github.com/iDvel/rime-ice/issues/133;
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

patch 语法示例

  • 注意:对于列表类型的字段值, patch 时必须列出修改后的整个列表值,不支持部分 patch。
# 以 patch: 开头,后面的内容都需要缩进
patch:

  ##### 修改单项
  # 正确 ✅ 这种方式只覆盖 Shift_L,不影响其他选项
  ascii_composer/switch_key/Shift_L: commit_code

  # 错误 ❌ 这样导致 switch_key 下将只有 Shift_L 一个选项
  ascii_composer/switch_key:
    Shift_L: commit_code

  ##### 如果有较多修改项,可以直接全部复制过来再修改
  ascii_composer:
    good_old_caps_lock: false
    switch_key:
      Caps_Lock: commit_code
      Shift_L: commit_code
      Shift_R: noop
      Control_L: noop
      Control_R: noop

  ##### 结尾的 /+ 表示在原基础上追加
  # 保留已有的快捷键,追加一个逗号句号翻页
  key_binder/bindings/+:
    - { when: paging, accept: comma, send: Page_Up }
    - { when: has_menu, accept: period, send: Page_Down }

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

  • 自定义短语:向自定义短语词典文件 opsnull_custom_phrase.txt 添加自定义短语,custom_prase/db_class 为stabledb,是只读的,不会动态调频。(可以设置为 tabledb 来动态调频)。
  • 首次添加该文件后需要执行 M-x rime-deploy 和 M-x rime-sync 生效。
patch:
  switches:
  - name: ascii_mode
    states: [ 中, A ]
  - name: ascii_punct  # 中英标点
    states: [ ¥, $ ]
  # 下面这些开关一般用不到, 故关闭(如候选词中不再显示 emoji).
  # - name: traditionalization
  #   states: [ 简, 繁 ]
  #   reset: 0
  # - name: emoji
  #   states: [ 💀, 😄 ]
  #   reset: 1
  # - name: full_shape
  #   states: [ 半角, 全角 ]
  #   reset: 0
  # - name: search_single_char  # search.lua 的功能开关,辅码查词时是否单字优先
  #   abbrev: [词, 单]
  #   states: [正常, 单字]
  #   reset: 0

  translator/spelling_hints: 0           # 不显示候选词的拼音。
  translator/always_show_comments: false #不显示候选者的拼音。
  translator/enable_user_dict: true      # 根据上屏自动调整词频, 否则根据 *.dict.yaml 中的静态定义的词频率。
  custom_phrase/user_dict: "opsnull_custom_phrase"  # 自定义短语词典文件,权重最高。

  speller/algebra:
  # 模糊拼音
  # 声母
  - derive/^([zcs])h/$1/          # z c s → zh ch sh
  - derive/^([zcs])([^h])/$1h$2/  # zh ch sh → z c s
  #- derive/^l/n/  # n → l
  #- derive/^n/l/  # l → n
  # 韵母
  - derive/eng$/en/
  - derive/en$/eng/
  - derive/in/ing/
  - derive/ing/in/

  # 自动纠错(后者用前者替换)
  # ai
  - derive/^([wghk])ai$/$1ia/  # wia → wai
  # ei
  - derive/([wfghkz])ei$/$1ie/  # wie → wei
  # ie
  - derive/([jqx])ie$/$1ei/  # jei → jie

Rime 输入法全局配置:

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@29/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"))))

个人词频:用户词典类型 translator/db_class 的值默认为 userdb,即二进制文件,输入过的内容会记录在 ~/Library/Rime/*.userdb/ 文件夹中,只有在同步后才能在同步目录 sync_dir/*/*userdb.txt 看到人类可读的用户词典;

  • M-x rime-sync 或点击鼠须管「同步用户数据」,Rime 将输入法方案的用户数据 *.userdb 与备份目录 sync_dir 进行双向更新同步。
# installation.yaml 文件在第一次部署后会自动生成,在这里可以编辑当前设备的 ID 和同步目录。

#本机的 ID 标志,默认是一串 UUID,生成的文件夹是这个名字,可以改成更好识别的名称。
installation_id: "cde8ff26-5e08-466c-bd2d-aac2aeaedb25"
# 同步的目标路径。
sync_dir: /Users/alizj/.emacs.d/sync/rime

userdb 不支持删除记录,所以不能通过清理 *userdb.txt 文件的方式来清理 userdb 记录。解决步骤是::

  1. 删除 ~/Library/Rime/*.userdb/ 目录;
  2. 重启 Emacs;
  3. 再执行 M-x rime-sync 来全新同步 *userdb.txt 中记录;

上面的步骤也适合于 userdb 文件损坏(查看文件 ~/Library/Rime/rime_ice.userdb/LOG)导致的个人词频不生效的情况。

8 completion
#

vertico 提供 minibuffer 区域的自动补全功能, 使用 orderless 的过滤风格来对候选者进行过滤:

  • corfu 提供的是光标出的自动补全;
  • C-] (abort-recursive-edit) 命令可以在任意 buffer 关闭 minibuffer 的编辑模式。
  • 如果要插入不存在的对象,例如新建一个 file 或 buffer, 可以使用 M-RET 快捷键(vertico-exit-input);
  • forward-paragraph -> vertico-next-group, 也即可以使用 M-} 来选择候选者列表中的下一个分组,例如不同的 file 或 project。
  • TAB -> vertico-insert
(use-package vertico
  :config
  (require 'vertico-directory)
  (setq vertico-count 20)
  ;; 默认不选中任何候选者,这样可以避免不必要的预览.
  ;;(setq vertico-preselect 'prompt)
  (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 结合使用, 使用 orderless 的过滤风格来过滤候选者.

  • 对于光标处的连续输入, 可以使用 M-SPC(corfu-insert-separator) 来插入 orderless 分隔符(默认是空格);
  • vetico 是 minibuffer 区域的补全 UI, 它直接使用 SPC(orderless 默认的分隔符) 分割多个过滤条件;
(use-package corfu
  :init
  (global-corfu-mode 1)    ;; 全局模式,eshell 等也会生效。
  (corfu-popupinfo-mode 1) ;;  显示候选者文档。
  ;; 滚动显示 corfu-popupinfo 中的内容, 与后续滚动显示 eldoc-box 中的内容操作一致。
  :bind (: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 140)
  (corfu-popupinfo-max-height 30)
  :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)

  ;; eshell 使用 pcomplete 来自动补全,eshell 自动补全。
  (add-hook 'eshell-mode-hook
            (lambda ()
              (setq-local corfu-auto nil)
              (corfu-mode)))
  )

;; 保存 corfu 自动补全历史,后续可以按照高频排序。
(savehist-mode 1)
(add-to-list 'savehist-additional-variables #'corfu-history)

;; minibuffer 历史记录。
(use-package savehist
  :hook (after-init . savehist-mode)
  :config
  (setq history-length 600)
  (setq savehist-save-minibuffer-history t)
  (setq savehist-autosave-interval 300)
  (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
  ;; 总是在弹出菜单中显示候选者。 TAB cycle if there are only few candidates
  (setq completion-cycle-threshold nil)
  ;; 使用 TAB 来 indentation+completion(completion-at-point 默认是 M-TAB) 。
  (setq tab-always-indent 'complete))

;; (use-package kind-icon
;;   :after corfu
;;   :demand
;;   :custom
;;   (kind-icon-default-face 'corfu-default)
;;   :config
;;   (add-to-list 'corfu-margin-formatters #'kind-icon-margin-formatter))

orderless 补全风格:使用空格分割的一个或多个匹配模式,模式的顺序没有关系,但是 AND 关系。默认情况下 orderless 使用 orderless-matching-styles 变量配置的 正则和字面量 匹配方式. 通过给各模式指定前缀或后缀, 也可以灵活指定其它匹配模式:

!
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.
,
uses orderless-initialism, 即首字母缩写匹配: \<a.*\<b.*\c;
=
uses orderless-literal, 字面量匹配;
~
uses orderless-flex, 匹配: a.*b.*c;
^
uses orderless-literal-prefix
&
modifies the component with orderless-annotation
%
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)
  ;; 进一步设置各 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-1052843656
          (eglot (styles . (orderless basic))) ;;使用 M-SPC 来分隔光标处的多个筛选条件。
          (eglot-capf (styles . (orderless basic)))
	  ))
  ;; 使用 SPACE 来分割过滤字符串, SPACE 可以用 \ 转义。
  (setq orderless-component-separator #'orderless-escapable-split-on-space))
  • partial-completion 支持 shell wildcards 和部分文件路径,如 /u/s/l for /usr/share/local;
  • 已知的 completion categories;

cape 补全融合:

(use-package cape
  :init
  ;; completion-at-point 使用的函数列表,注意顺序。
  (add-to-list 'completion-at-point-functions #'cape-file)
  ;;(add-to-list 'completion-at-point-functions #'cape-dabbrev)
  (add-to-list 'completion-at-point-functions #'cape-elisp-block)
  ;;(add-to-list 'completion-at-point-functions #'cape-symbol)
  ;;(add-to-list 'completion-at-point-functions #'cape-keyword)
  ;;(add-to-list 'completion-at-point-functions #'cape-history)
  ;;(add-to-list 'completion-at-point-functions #'cape-tex)
  ;;(add-to-list 'completion-at-point-functions #'cape-sgml)
  ;;(add-to-list 'completion-at-point-functions #'cape-rfc1345)
  ;;(add-to-list 'completion-at-point-functions #'cape-abbrev)
  ;;(add-to-list 'completion-at-point-functions #'cape-dict)
  ;;(add-to-list 'completion-at-point-functions #'cape-line)
  :config
  (setq dabbrev-check-other-buffers nil
        dabbrev-check-all-buffers nil
        cape-dabbrev-min-length 3)
  ;; 前缀长度达到 3 时才调用 CAPF,避免频繁调用自动补全。
  (cape-wrap-prefix-length #'cape-dabbrev 3)
  ;; 持续刷新候选者(适用于 eglot server 一次没有返回所有候选者情况).
  ;; profiling 显示影响性能,展示关闭。
  ;;(advice-add 'eglot-completion-at-point :around #'cape-wrap-buster)
  )

安装 ripgrep 工具命令,consult-rg 依赖它:

which rg || brew install ripgrep

配置 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)

  ;; 使用 consult 来预览 xref 的引用定义和跳转。
  (setq xref-show-xrefs-function #'consult-xref)
  (setq xref-show-definitions-function #'consult-xref)

  ;; 不搜索 go vendor 目录。
  (setq consult-ripgrep-args
        "rg --null --line-buffered --color=never --max-columns=1000 --path-separator / --smart-case --no-heading --with-filename --line-number --search-zip -g !vendor/")
  :config
  ;; 按 C-l 激活预览,否则 Buffer 列表中有大文件或远程文件时会卡住。
  (setq consult-preview-key "C-l")
  ;; Use minibuffer completion as the UI for completion-at-point. 也可
  ;; 以使用 Corfu 或 Company 等直接在 buffer中 popup 显示补全。
  (setq completion-in-region-function #'consult-completion-in-region)
  ;; 不对 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)
(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)
(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、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)
(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)
(global-set-key (kbd "M-g i") #'consult-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)
(global-set-key (kbd "M-s l") #'consult-line)
(global-set-key (kbd "M-s M-l") #'consult-line)
;; Search dynamically across multiple buffers. By default search across project buffers. If invoked
;; with a prefix argument search across all 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-buffer 显示的 File 列表来源于变量 recentf-list;

consult-buffer 操作: consult-buffer (-other-window, -other-frame) , 支持过滤不同 buffer 类型:

  • b Buffers (consult-buffer)
  • SPC Hidden buffers
  • * Modified buffers
  • f Files (Requires recentf-mode, consult-recent-file)
  • r File registers
  • m Bookmarks (C-x r b, consult-bookmark)
  • p Project (C-x p b, consult-project-buffer): 显示 project 相关的 buffers 和 files。

grep 和 find: 支持异步搜索和实时过滤

  • consult-grep, consult-ripgrep, consult-git-grep: 根据正则表达式搜索文件内容;
  • consult-find, consult-locate: 根据正则表达式搜索文件名称;
  • 默认在当前 project 搜索,加 C-u 前缀,可以指定搜索目录。

两级搜索模式,用 # 来标识开始和结束,例如 #regexp1 regexp2#consult:

  • 第一级:支持 – 来分割搜索正则表达式和传递给 grep/riggrep/find 的参数,例如:#defun – –invert-match#;
  • 第二级:使用空格分割的 orderless 补全过滤风格,这部分补全字符串不传递给 grep/ripgrep/find, 纯粹是 orderless buffer 过滤;
  • 第一级用空格分隔多个 regexp, 它们之间是 AND 关系,空格本身可以用 \ 转义, 正则表达式使用 Emacs regexp 语法,例如 #\(consult\|embark\),consult 自动转换为 grep/ripgrep/find 的正则语法;

embark 为 minibuffer 或当前 buffer 选中的内容提供一个快捷操作命令(一般是单字符命令)embark-act(快捷键 C-;):

(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 可以使用的快捷键。
  (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))

Embark Collect:在通用的 Embark collect buffer 中对一批候选对象、搜索结果列表等进行操作。

  • embark-collect-snapshot(S):在 Embark Collect Buffer 中显示候选情况,不更新 Buffer 内容;
  • embark-collect-live(L):根据候选情况,实时更新 Embark Collect Live Buffer 中的内容;

Embark Collect Buffer 类似于 dired, you can mark and unmark candidates with m and u, you can unmark all marked candidates with U or toggle the marks with t. In an Embark Collect buffer embark-act-all is bound to A and will act on all currently marked candidates if there any, and will act on all candidates if none are marked.

  • 先使用 Embark Collect 来收集候选者,使用 mark 标记多个候选者,然后使用 A 来对候选者执行 embark-act 操作。

Embark Export(E):根据当前候选者的不同(可以使用 b/f/m SPC 来缩小类型范围),将结果显示在不同的 Buffer 中:

  • Dired: 如果候选者是文件,则将结果显示到 Dired Buffer 中;
  • Embark Export Ibuffer: 如果候选者是 Buffer;
  • Embark Export Grep: 对 consult-grep、consult-git-grep、consult-ripgrep 等搜索结果进行 export 时,进入 Embark Export Grep buffer,使用 C-c C-p 切换到 wgrep 模式来对结果进行批量编辑;
  • Embark Export Occur: consult-line 的结果会被 export 到 occur-mode;

对于 Collect 和 Export:优选 Export, 因为它能根据候选者的类型 export 到合适的 buffer 类型中。

在显示 Act 的时候,除了按列出的快捷键外,还可以:

C-;
切换 Act 类型;
C-h
使用 Minibuffer 候选列表来根据输入进行过滤选择 Action;

各种缺省的 Actions: https://github.com/oantolin/embark/wiki/Default-Actions

marginalia:

(use-package marginalia
  :init
  ;; 显示绝对时间。
  (setq marginalia-max-relative-age 0)
  (marginalia-mode))

9 org
#

安装 watchexec 工具:

which watchexec || brew install watchexec

配置 org:

(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 t
        ;; export 时不处理 super/subscripting, 等效于 #+OPTIONS: ^:nil 。
        org-export-with-sub-superscripts nil

        ;; 使用 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
        ;; 如果对 headline 编号则 latext 输出时会导致 toc 缺失,故关闭。
        org-startup-numerated nil
        org-startup-indented t

        ;; 先从 #+ATTR.* 获取宽度,如果没有设置则默认为 300 。
        org-image-actual-width '(300)
        org-cycle-inline-images-display nil

        ;; org-timer 到期时发送声音提示。
        org-clock-sound t)

  ;; 不自动对齐 tag。
  (setq org-tags-column 0)
  (setq org-auto-align-tags nil)
  ;; 显示不可见的编辑。
  (setq org-catch-invisible-edits 'show-and-error)
  (setq org-fold-catch-invisible-edits t)
  (setq org-special-ctrl-a/e t)
  (setq org-insert-heading-respect-content t)
  ;; 支持 ID property 作为 internal link target(默认是 CUSTOM_ID property)
  (setq org-id-link-to-org-use-id t)
  (setq org-M-RET-may-split-line nil)
  (setq org-todo-keywords '((sequence "TODO(t!)" "DOING(d@)" "|" "DONE(D)")
                            (sequence "WAITING(w@/!)" "NEXT(n!/!)" "SOMEDAY(S)" "|" "CANCELLED(c@/!)")))
  (add-hook 'org-mode-hook 'turn-on-auto-fill)
  (add-hook 'org-mode-hook (lambda () (display-line-numbers-mode 0))))

;; 关闭与 sis 冲突的 C-, 快捷键。
(define-key org-mode-map (kbd "C-,") nil)
(define-key org-mode-map (kbd "C-'") nil)

(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-element-cache 警告 buffer 。
(setq org-element-use-cache 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)

(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 nil)
  (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 相关目录。
(dolist (dir '("~/docs/org" "~/docs/org/journal"))
  (unless (file-directory-p dir)
    (make-directory dir)))
which pngpaste || brew install pngpaste
which magick || brew install imagemagick
  • imagemagick 用于图片分辨率转换, 编译 emacs 时需要指定 --with-imagemagick 参数。

org-download:拖拽保存图片或 F6 保存剪贴板中图片:

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

配置 babel:

;; 关闭 C-c C-c 触发执行代码.
;;(setq org-babel-no-eval-on-ctrl-c-ctrl-c t)
;; 关闭确认执行代码的操作.
(setq org-confirm-babel-evaluate nil)
;; 使用语言的 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) ;; provide C, C++, and D
   (java . t)
   (awk . t)
   (css . t)))

(use-package org-contrib)

~/emacs/templates 文件中添加一个名为 my-latext 的 tempel 模板,内容如下:

  • 如果生成的 pdf 不显示目录,检查文档 #+OPTIONS 参数中的 toc:nil 和 num: 2 是否生效(如在对应行上执行 C-c C-c)。
(my-latex "#+DATE: " (format-time-string "%Y-%m-%d %a") n
	  "#+SUBTITLE: 内部资料,注意保密!
#+AUTHOR: 张俊([email protected])
# 中文语言环境(目录等用中文显示)。
#+LANGUAGE: zh-CN
# 不自动输出 titile 和 toc,后续 latext mystyle 中定制输出。
# 但是需要明确通过 num 控制输出的目录级别。
#+OPTIONS: prop:t title:nil num:2 toc:nil ^:nil
#+LATEX_COMPILER: xelatex
#+LATEX_CLASS: ctexart
#+LATEX_HEADER: \\usepackage{/Users/alizj/emacs/mystyle}

# 定制 PDF 封面和目录。
#+begin_export latex
% 封面页
\\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
#+end_export
")

配置 tex:

;; 将安装的 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- 为对应的导出后端。
;;(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 日志文件(调试时打开)。
  (setq org-latex-remove-logfiles nil)
  ;; 使用支持中文的 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)

在线预览 LaTex fragments:

  • 预览命令:C-c C-x C-l (org-latex-preview),When called with a single prefix argument, clear all images in the current entry. Two prefix arguments produce a preview image for all fragments in the buffer, while three of them clear all the images in that buffer.
  • 或者添加启动时自动预览:#+STARTUP: latexpreview 或 nolatexpreview
;; 使用 imagemagick 而非默认的 dvipng 来生成 M-x org-latex-preview 在线显式的图片。
;; dvipng 总是报错。
;; 参考:https://orgmode.org/worg/org-tutorials/org-latex-preview.html
(setq latex-run-command "xelatex")
(setq org-latex-create-formula-image-program 'imagemagick)

自定义样式 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}{0.5em}
\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}}

slide:

  • 如果文字居中失效, 可以执行 M-x redraw-display 命令来生效。
(use-package org-tree-slide
  :after (org)
  :commands org-tree-slide-mode
  :hook
  ((org-tree-slide-play . (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)
                            (setq org-tree-slide-slide-in-waiting 0.01)
                            (read-only-mode 1)))
   (org-tree-slide-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 org-tree-slide-header t)
  (setq org-tree-slide-content-margin-top 0)
  (setq org-tree-slide-heading-emphasis nil)
  (setq org-tree-slide-slide-in-effect t)
  (setq org-tree-slide-activate-message " ")
  (setq org-tree-slide-deactivate-message " ")
  ;;(setq org-tree-slide-modeline-display t)
  ;;(setq org-tree-slide-breadcrumbs " 👉 ")
  (define-key org-mode-map (kbd "<f8>") #'org-tree-slide-mode)
  (define-key org-tree-slide-mode-map (kbd "<f9>") #'org-tree-slide-content)
  (define-key org-tree-slide-mode-map (kbd "<left>") #'org-tree-slide-move-previous-tree)
  (define-key org-tree-slide-mode-map (kbd "<right>") #'org-tree-slide-move-next-tree))

org-capture 支持 store-link 和 capture 协议:

  1. store-link:获取浏览器的 URL 和 Title,然后在 kill-ring 中生成一个链接;
  2. capture:根据浏览器复制的内容和指定的 capture-template 名称来创建一个 capture 项目。

打开 MAC “脚本编辑器” ,写入如下内容,保存为 “EmacsClient-Org”,文件格式为 “应用程序”,保存到 /Applications 目录。

on open location this_URL
    do shell script "/opt/homebrew/bin/emacsclient \"" & this_URL & "\" && open -a Emacs"
end open location
  • 如果是自编译的 Emmacs 则 emacsclient 位于 /opt/homebrew/bin/ 目录下,否则位于 /Applications/Emacs 包中。

编辑 “/Applications/EmacsClient-Org.app/Contents/Info.plist” 文件,在 plist->dict 部分添加如下内容:

  <key>CFBundleURLTypes</key>
  <array>
    <dict>
      <key>CFBundleURLName</key>
      <string>org-protocol handler</string>
      <key>CFBundleURLSchemes</key>
      <array>
        <string>org-protocol</string>
      </array>
    </dict>
  </array>

然后执行命令:

xattr -r -d com.apple.quarantine /Applications/EmacsClient-Org.app

双击刚才保存到应用程序目录中的 EmacsClient-Org 程序图标,激活 org-proto 协议。

保存浏览器链接:新建一个浏览器书签,Location 内容如下,然后点击该书签,确认 Emacs 有反应,在 Emacs 内按 C-c C-l 自动补全 URL 和 Title.

javascript:location.href='org-protocol://store-link?url='+encodeURIComponent(location.href)+'&title='+encodeURIComponent(document.title)

需要在浏览器分别建立对应的书签后而且 emacs 以 server 模式运行才可以使用,

(require 'org-protocol)
(require 'org-capture)

(setq org-capture-templates
      '(("c" "Capture" entry (file+headline "~/docs/org/capture.org" "Capture")
         "* %^{Title}\nDate: %U\nSource: %:annotation\nQuote:\n#+BEGIN_QUOTE\n%i\n#+END_QUOTE\n\n"
	 :empty-lines 1)
        ("t" "Todo" entry (file+headline "~/docs/org/todo.org" "Tasks")
         "* TODO %?\n %U %a\n %i"
	 :empty-lines 1)))

新建一个浏览器书签,内容如下:

javascript:location.href='org-protocol://capture?template=c'+'&url='+encodeURIComponent(window.location.href)+'&title='+encodeURIComponent(document.title)+'&body='+encodeURIComponent(window.getSelection())

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)

  (setq org-journal-file-type 'monthly)
  (setq org-journal-dir "~/docs/org/journal")
  (setq org-journal-find-file 'find-file)

  ;; 加密 journal 文件。
  (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)

  ;; journal 文件头。
  (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))

;; org-agenda 集成。
;; automatically adds the current and all future journal entries to the agenda
;;(setq org-journal-enable-agenda-integration t)
;; When org-journal-file-pattern has the default value, this would be the regex.
(setq org-agenda-file-regexp "\\`\\\([^.].*\\.org\\\|[0-9]\\\{8\\\}\\\(\\.gpg\\\)?\\\)\\'")
(add-to-list 'org-agenda-files org-journal-dir)

;; org-capture 集成。
(defun org-journal-find-location ()
  (org-journal-new-entry t)
  (unless (eq org-journal-file-type 'daily)
    (org-narrow-to-subtree))
  (goto-char (point-max)))
(setq org-capture-templates
      (cons '("j" "Journal" plain (function org-journal-find-location)
              "** %(format-time-string org-journal-time-format)%^{Title}\n%i%?"
              :jump-to-captured t :immediate-finish t) org-capture-templates))
  • 不开启 org-journal-enable-agenda-integration, 而是向 org-agenda-files 变量添加日志文件的方式。否则在历史日记被删除的情况下, 可能导致 Dashbard 显示 agenda 时 hang 。

对于 org-mode 文件,可使用 templ 模板在文件开头添加如下内容结间可避免每次打开时提示选择 gpg key:

;; 插入自己的 GnuPG 加密 key。
(my-gpg "# -*- mode:org; epa-file-encrypt-to: (\"[email protected]\") -*-")

ox-hugo 博客:

(use-package ox-hugo
  :demand
  :config
  (setq org-hugo-base-dir (expand-file-name "~/blog/local.view"))
  (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))

10 magit
#

(setq auto-revert-check-vc-info t) 自动 revert buffer,确保 modeline 上的分支名正确,但是 CPU Profile 显示比较影响性能,故暂不开启。

(setq vc-follow-symlinks 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 根据仓库地址、commit 等信息为光标位置生成 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)))))))
)

11 coding
#

11.1 indent
#

高亮显示缩进:

(use-package highlight-indent-guides
  :custom
  (highlight-indent-guides-method 'column)
  (highlight-indent-guides-responsive 'top)
  (highlight-indent-guides-suppress-auto-error t)
  :config
  (add-hook 'python-mode-hook 'highlight-indent-guides-mode)
  (add-hook 'python-ts-mode-hook 'highlight-indent-guides-mode)
  (add-hook 'yaml-mode-hook 'highlight-indent-guides-mode)
  (add-hook 'yaml-ts-mode-hook 'highlight-indent-guides-mode)
  (add-hook 'js-mode-hook 'highlight-indent-guides-mode)
  (add-hook 'js-ts-mode-hook 'highlight-indent-guides-mode)
  (add-hook 'web-mode-hook 'highlight-indent-guides-mode))

c/c++/go-mode indent 风格:总是使用 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)
(setq c-electric-pound-behavior 'alignleft)
;; 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)

11.2 paren
#

彩色括号:

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

智能补全括号:

(use-package smartparens
  :config
  (require 'smartparens-config)
  (add-hook 'prog-mode-hook #'smartparens-mode)
  ;;(smartparens-global-mode t)
  (show-smartparens-global-mode t))

11.3 project
#

(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)))
  (compilation-always-kill t)
  (project-vc-merge-submodules nil)
  :config
  ;; project-find-file 忽略的目录或文件列表。
  (add-to-list 'vc-directory-exclusion-list "vendor")
  (add-to-list 'vc-directory-exclusion-list "node_modules")
  (add-to-list 'vc-directory-exclusion-list "target"))

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

手动添加 project 目录: M-x project-remember-projects-under

11.4 treesit
#

treesit-auto 自动安装 grammer 和自动将 xx 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.

11.5 flymake
#

flymake 检查 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 不会显示实时的诊断消息,而是当保存 buffer 后经过 eglot-send-changes-idle-time 时间后才显示 LSP 诊断消息,这样可以避免显示无意义的错误。

(use-package flymake
  :config
  (setq flymake-no-changes-timeout nil) ;; 不自动检查 buffer 错误。
  (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))
  • M-x flymake-show-buffer-diagnostics
  • M-x flymake-show-project-diagnostics

调试 flymake:

  1. 先设置 log 级别变量: warning-minimum-log-level 和 warning-minimum-level;
  2. 执行命令: M-x flymake-switch-to-log-buffer
  3. 查看当前注册的 backend: flymake-diagnostic-functions.
  4. 其他 backend 命令: flymake-reporting-backends, flymake-running-backends and flymake-disabled-backends

11.6 eldoc
#

eglot 不指示 eldoc 在 echo-area 显示结构化成员(field) 或函数签名信息, 但是在 M-x eldoc-doc-buffer(C-h-.) 打开的 eldoc buffer 中会显示这些信息.

(use-package eldoc
  :config
  (setq eldoc-idle-delay 0.1)
  ;; eldoc 支持多个 document sources, 默认当它们都 Ready 时才显示, 设置为 compose-eagerly 后会显示先
  ;; Ready 的内容.
  ;;(setq eldoc-documentation-strategy 'eldoc-documentation-compose-eagerly)

  ;; 在打开 eldoc-buffer 时关闭 echo-area 显示, eldoc-buffer 的内容会跟随显示 hover 信息, 如函数签名.
  (setq eldoc-echo-area-prefer-doc-buffer t)

  ;; (add-to-list 'display-buffer-alist
  ;;                '("^\\*eldoc.*\\*"
  ;;                 (display-buffer-reuse-window display-buffer-in-side-window)
  ;;                 (dedicated . t)
  ;;                 (side . right)
  ;;                 (inhibit-same-window . t)))

  ;; 一键显示和关闭 eldoc buffer:
  (global-set-key (kbd "M-`")
                  (
                   lambda()
                   (interactive)
                   (if (get-buffer-window "*eldoc*")
                       (delete-window (get-buffer-window "*eldoc*"))
                     (display-buffer "*eldoc*")))))

eldoc-box 在 frame 右上角显示 eldoc-doc-buffer 的内容. 依赖 markdown-mode 来格式化显示文档的内容, 但是不能点击其中的链接, https://github.com/joaotavora/eglot/discussions/1238

(use-package eldoc-box
  :after
  (eglot eldoc)
  ;; 滚动显示 eldoc-box buffer 中的内容, 与 corfu-popupinfo-map 的操作一致:
  :bind (:map eglot-mode-map
              ("C-M-k" . my/eldoc-box-scroll-up)
              ("C-M-j" . my/eldoc-box-scroll-down)
              ("M-h" . eldoc-box-eglot-help-at-point))
  :config
  (setq eldoc-box-max-pixel-height 600)
  (defun my/eldoc-box-scroll-up ()
    "Scroll up in `eldoc-box--frame'"
    (interactive)
    (with-current-buffer eldoc-box--buffer
      (with-selected-frame eldoc-box--frame
        (scroll-down 3))))
  (defun my/eldoc-box-scroll-down ()
    "Scroll down in `eldoc-box--frame'"
    (interactive)
    (with-current-buffer eldoc-box--buffer
      (with-selected-frame eldoc-box--frame
        (scroll-up 3))))

  (add-hook 'eglot-managed-mode-hook #'eldoc-box-hover-mode t)
  ;; eldoc-box-hover-at-point-mode 有性能问题,显示延迟大, 故不使用.
  ;;(add-hook 'eglot-managed-mode-hook #'eldoc-box-hover-at-point-mode t)
  )

由于使用了 eldoc-box 显示 eldoc 信息,所以没必要再在 minibuffer 显示 eldoc 信息。这里将 minibuffer 窗口最大高度设为 1,可以确保显示一行(默认为小数,表示 frame 高度占比,会导致显示多行)。

(setq max-mini-window-height 1)
;; 为 nil 时只单行显示 eldoc 信息.
(setq eldoc-echo-area-use-multiline-p nil)

11.7 eglot
#

elgot 使用 emacs 内置的 flymake(而非 flycheck)、xref、eldoc、project。

eglot 使用 Emacs 内置的 flymake 而非 flycheck 来接收和显示 LSP Server 发送的 publishDiagnostics 事件, eglot 诊断是通过向 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。

不能给所有 prog-mode 都开启 eglot,否则当它没有 language server 时 eglot 报错。

由于 treesit-auto 已经对 major-mode 做了 remap ,需要对 xx-ts-mode-hook 添加 hook,而不是以前的 xx-mode-hook, 否则添加到 xx-mode-hook 的内容不会被自动执行.

配置 emacs eglot:

(use-package eglot
  :demand
  :after
  (flymake eldoc)
  :preface
  ;; 由于后续 eglot 将 flymake stay-out,需要手动加回 eglot-flymake-backend 并启动 flymake。
  (defun my/manually-activate-flymake ()
    (add-hook 'flymake-diagnostic-functions #'eglot-flymake-backend nil t)
    (flymake-mode 1))

  (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)))
    ;; (setq eldoc-documentation-strategy 'eldoc-documentation-compose-eagerly)
    )
  :hook (
         (eglot-managed-mode . my/eglot-eldoc)
         (eglot-managed-mode . my/manually-activate-flymake)
         )
  :bind
  (:map eglot-mode-map
        ("C-c C-a" . eglot-code-actions)
        ;; 如果 buffer 出现错误的诊断消息,执行 flymake-start 重新触发诊断。
        ("C-c C-c" . flymake-start)
        ("C-c C-d" . eldoc)
        ("C-c C-p" . eldoc-box-help-at-point) ;; 显示光标处的帮助信息.
        ("C-c C-f" . eglot-format-buffer)
        ("C-c C-r" . eglot-rename))
  :config
  ;; elgot 不管理和配置 flymake,这样会保留自定义的 flymake backend(如 flymake-clippy)。
  (add-to-list 'eglot-stay-out-of 'flymake)

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

  (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)。
  (setq-default eglot-workspace-configuration
                '(
                  ;; gopls 配置参数: https://github.com/golang/tools/blob/master/gopls/doc/settings.md
                  (:gopls . (
                             (staticcheck . t)
                             (usePlaceholders . :json-false)
                             ;; gopls 默认设置 GOPROXY=Off, 可能会导致 package 缺失进而引起补全异常.
                             ;; 开启 allowImplicitNetworkAccess 后将关闭 GOPROXY=Off.
                             (allowImplicitNetworkAccess . t)
                             )))))

调试 eglot, 先切换到源码文件 buffer:

  • 调大 eglot-events-buffer-size 变量值;
  • 查看 language server 的 stderr 信息: M-x eglot-stderr-buffer
  • 查看 language server 的访问日志: M-x eglot-events-buffer

eglot 使用 project-current 来获得 project root 目录,如果该函数返回的目录不对,可能会导致 eglot 补全失效, 报错:

jsonrpc-error: "request id=44 failed:", (jsonrpc-error-code . 0), (jsonrpc-error-message . "no package metadata for file file:///Users/zhangjun/go/bin/pkg/mod/k8s.io/klog/[email protected]/klog.go"), (jsonrpc-error-data)

可以查看变量 eglot--servers-by-project 来查看当前 eglot server 识别的 project 情况:

  • 如果代码项目没有 .git 目录,则打开文件时可能会卡住。
eglot--servers-by-project is a variable defined in eglot.el.

Value
#s(hash-table size 65 test equal rehash-size 1.5 rehash-threshold 0.8125 data
	      ((local . "/usr/local/go/src/")
	       nil
	       (local . "~/go/bin/")
	       nil
	       (local . "/usr/local/Cellar/go/1.20.5/libexec/src/")
	       nil
	       (local . "~/go/src/github.com/opsnull/learn-by-doing/log/klog/")
	       nil
	       (local . "~/go/src/github.com/opsnull/learn-by-doing/log/zap/")

自定义函数 my/project-try-local 函数来返回非 git 的 project 的 root 目录,它顺着目录 top-down 查找第一个匹配的文件。所以,中间目录不能有 .project 等文件,否则判断 project root 失败。可以使用如下命令来确认是否定位准确: (my/project-try-local "/path/to/directory") ,如果定位不准确,可以调整 pr-flags 参数列表中的文件名顺序来尝试解决。

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 (:fetcher github :repo jdtsmith/eglot-booster)
  :after (eglot)
  :config (eglot-booster-mode))

11.8 python
#

brew install python 目前(2024.03.17)安装的是 python@12 版本:

brew reinstall python
brew unlink [email protected] && brew link [email protected]
# 查看安装的位置
ls -l $(brew --prefix python)/libexec/bin

$ pip3 install  pygments
error: externally-managed-environment

× This environment is externally managed
╰─> To install Python packages system-wide, try brew install
    xyz, where xyz is the package you are trying to
    install.

    If you wish to install a non-brew-packaged Python package,
    create a virtual environment using python3 -m venv path/to/venv.
    Then use path/to/venv/bin/python and path/to/venv/bin/pip.

    If you wish to install a non-brew packaged Python application,
    it may be easiest to use pipx install xyz, which will manage a
    virtual environment for you. Make sure you have pipx installed.

note: If you believe this is a mistake, please contact your Python installation or OS distribution provider. You can override this, at the risk of breaking your Python installation or OS, by passing --break-system-packages.
hint: See PEP 668 for the detailed specification.

创建一个 ~/.venv python 虚拟环境, 然后将 pip 包安装到该环境中:

zj@a:~$ python3 -m venv .venv
zj@a:~$ source ~/.venv/bin/activate
# 更新 ~/.bashrc 中的 PATH: PATH=/Users/alizj/.venv/bin/:$PATH

# 安装相关的包到虚拟环境中
(.venv) zj@a:~$ pip3 install pygments jinji2 ipython markdown flake8 yapf pyright grip debugpy

配置 Emacs 使用 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:
(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"))))

;; 使用 yapf 格式化 python 代码。
(use-package yapfify)

;; 使用内置的 python mode.
(use-package python
  :init
  (defvar pyright-directory "~/.emacs.d/.cache/lsp/npm/pyright/lib")
  (if (not (file-exists-p pyright-directory))
      (make-directory pyright-directory t))
  ;;(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)
                   (yapf-mode))))

微软不再维护 python-language-server(pylsp),主力发展 pyright 和 pyglance,所以不再使用 lsp-python-ms 和pyls,而使用 lsp-pyright。

  • python-lanuage-server 的活跃 fork 版本: https://github.com/python-lsp/python-lsp-server
  • lsp-pyright 是 lsp-mode 的 pyright emacs client, 在使用 lsp-bridge 后,只需要安装 pyright npm 包即可,不需要再安装 lsp-pyright.

安装 pyright:

which pyright || npm update -g pyright

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 File testState.test.ts

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

修改 pyrightconfig.json 后,需要执行 M-x lsp-workspace-restart 来重启 lsp,如果还是有问题,则可以查看 *lsp-log* buffer 的日志。

11.9 go
#

安装最新 gopls 工具:

go install golang.org/x/tools/gopls@latest

使用 Emacs 内置的 go-ts-mode, 故不需要再单独安装 go-mode package.

设置 go 环境变量, eglot 启动 gopls 时传递它们:

(dolist (env '(("GOPATH" "/Users/alizj/go")
               ("GOPROXY" "https://goproxy.cn,https://goproxy.io,direct")
               ("GOPRIVATE" "*.alibaba-inc.com")))
  (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 搜索文档.
(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 o") 'my/browser-pkggo) ;; 助记: o -> online

安装或更新工具:

(require 'go-ts-mode)
;; go 使用 TAB 缩进.
(add-hook 'go-ts-mode-hook (lambda () (setq indent-tabs-mode t)))

(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/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
  1. 如果补全或自动提示异常, 执行 M-x eglot-events-buffer 看是否有报错(例如 GOPROXY=Off 导致的问题.)

11.10 rust
#

安装 rust 工具链,这里使用 rustup 来管理工具链和版本:

# 清理旧环境
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 rust-src
rustup component add rust-docs # 添加 rust 标准库文档
rustup toolchain list   # 查看安装的工具链

查看文档:

rustup component add rust-docs # 添加 rust 标准库文档
rustup doc # 查看标准库文档
rustup doc topic # 查看某个 topic 的帮助文档,如 core,fn,std:char 等。
cargo doc --open # 查看当前项目和依赖的文档

清理 crate:

cargo install cargo-sweep # 清理没有使用的 crate。
cargo sweep --time 30
cargo sweep --installed

将 brew rustup-init 安装的目录添加到 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)
  (setq rust-mode-treesitter-derive t) ;; rust-mode 作为 rust-ts-mode 而非 prog-mode 的子 mode.
  :config
  (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
                  ( ;;:checkOnSave :json-false ;; 保存文件时不检查(有诊断就够了).
                   :cachePriming (:enable :json-false) ;; 启动时不预热缓存.
                   ;;https://esp-rs.github.io/book/tooling/visual-studio-code.html#using-rust-analyzer-with-no_std
                   :check (
                           :command "clippy"
                           :allTargets :json-false
                           :workspace  :json-false ;; 不发送 --workspace 给 cargo check, 只检查当前 package.
                           )
                   :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"])
                   )
                  )))
  )

flymake-clippy 为 flymake 添加 flymake-clippy-backend,用于对 rust 代码进行丰富的 linter 规则检查:

(use-package flymake-clippy
  :after (flymake rust-mode)
  :hook
  (rust-ts-mode . flymake-clippy-setup-backend))

快速 rust 开发测试:

  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 = [\"Rust Example <[email protected]>\"]
edition = \"2021\"

[dependencies]"))

eglot-x 为 rust 提供了 M-x eglot-x-reload-workspace 命令, 可以在 Cargo.toml 文件发生变化时手动执行而不需要重启 eglot:

(use-package eglot-x
  :after (eglot rust-mode)
  :vc (:fetcher github :repo 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 文档.
  (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 o") 'my/browser-docsrs) ;; 助记: o -> online
  )

cargo package 不再维护, 故切换到 cargo-mode package, 它提供了管理 Cargo.toml 依赖的命令.

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

carg-mode 命令(快捷键前缀: C-c a):

  • C-c a e - cargo-execute-task - List all available tasks and execute one of them. As a bonus, you’ll get a documentation string because cargo-mode.el parses shell output of cargo –list directly.
  • C-c a t - cargo-mode-test - Run all tests in the project (cargo test).
  • C-c a l - cargo-mode-last-command - Execute the last executed command.
  • C-c a b - cargo-mode-build - Build the project (cargo build).
  • C-c a o - cargo-mode-test-current-buffer - Run all tests in the current buffer.
  • C-c a f - cargo-mode-test-current-test - Run the current test where pointer is located.

其他技巧:

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

11.11 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/[email protected]/dist/mermaid.min.js'></script>
<script>
mermaid.initialize({
  theme: 'default',  // default, forest, dark, neutral
  startOnLoad: true
});
</script>
"))

使用 grip 来预览 markdown 文件,它调用 github markdown API 来渲染文件,从而确保渲染后分隔和 Github 一致。为了避免 API 调用频率限制,可以创建一个空 scop 的 Access Token,然后将 username 和 token 保存到 ~/.authinfo.gpg 文件中:

machine api.github.com login [email protected] password YOUR_TOKEN

在 Markdown Buffer 中,执行 M-x grip-mode 来启用实时预览,然后可以执行如下命令:

  • M-x grip-start-preview
  • M-x grip-stop-preview
  • M-x grip-restart-preview
  • M-x grip-browse-preview 使用浏览器来预览
(use-package grip-mode
  :defer
  :after (markdown-mode)
  :config
  (setq grip-preview-use-webkit nil)
  (setq grip-preview-host "127.0.0.1")
  ;; 保存文件时才更新预览。
  (setq grip-update-after-change nil)
  ;; 从 ~/.authinfo 文件获取认证信息。
  (require 'auth-source)
  (let ((credential (auth-source-user-and-password "api.github.com")))
    (setq grip-github-user (car credential)
          grip-github-password (cadr credential)))
  (define-key markdown-mode-command-map (kbd "g") #'grip-mode))

为 markdown 文件添加目录:

(use-package markdown-toc
  :after(markdown-mode)
  :config
  (define-key markdown-mode-command-map (kbd "r") #'markdown-toc-generate-or-refresh-toc))

11.12 shell
#

emacs 使用 bash-ts-mode 来编辑 shell 脚本。安装 bash language server:

npm i -g bash-language-server

bash language server 使用 shellcheck 工具来做语法检查和静态分析,使用 lsp diagnose 机制来提示错误:

brew install shellcheck

设置脚本缩进规则:

(setq sh-basic-offset 4)
(setq sh-indentation 4)

参考:

  1. Google Shell Style Guide

11.13 clang
#

安装最新的 llvm 和 clang:

brew install llvm

LDFLAGS="-L/opt/homebrew/opt/llvm/lib/c++ -Wl,-rpath,/opt/homebrew/opt/llvm/lib/c++"
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 目录创建项目配置文件:

  • 主要修改的是:Tab 和 Indent 的配置参数。
# clang-format configuration file. Intended for clang-format >= 11.
#
# For more information, see:
#
#   Documentation/process/clang-format.rst
#   https://clang.llvm.org/docs/ClangFormat.html
#   https://clang.llvm.org/docs/ClangFormatStyleOptions.html

# linux 内核开发风格:
# https://raw.githubusercontent.com/torvalds/linux/master/.clang-format
---
DisableFormat: false
TabWidth: 8
UseTab: Always
IndentWidth: 8

AccessModifierOffset: -4
AlignAfterOpenBracket: Align
AlignConsecutiveAssignments: false
AlignConsecutiveDeclarations: false
AlignEscapedNewlines: Left
AlignOperands: true
AlignTrailingComments: false
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortBlocksOnASingleLine: false
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: None
AllowShortIfStatementsOnASingleLine: false
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: false
AlwaysBreakTemplateDeclarations: false
BinPackArguments: true
BinPackParameters: true
BraceWrapping:
  AfterClass: false
  AfterControlStatement: false
  AfterEnum: false
  AfterFunction: true
  AfterNamespace: true
  AfterObjCDeclaration: false
  AfterStruct: false
  AfterUnion: false
  AfterExternBlock: false
  BeforeCatch: false
  BeforeElse: false
  IndentBraces: false
  SplitEmptyFunction: true
  SplitEmptyRecord: true
  SplitEmptyNamespace: true
BreakBeforeBinaryOperators: None
BreakBeforeBraces: Custom
BreakBeforeInheritanceComma: false
BreakBeforeTernaryOperators: false
BreakConstructorInitializersBeforeComma: false
BreakConstructorInitializers: BeforeComma
BreakAfterJavaFieldAnnotations: false
BreakStringLiterals: false
ColumnLimit: 80
CommentPragmas: '^ IWYU pragma:'
CompactNamespaces: false
ConstructorInitializerAllOnOneLineOrOnePerLine: false
ConstructorInitializerIndentWidth: 8
ContinuationIndentWidth: 8
Cpp11BracedListStyle: false
DerivePointerAlignment: false

ExperimentalAutoDetectBinPacking: false
FixNamespaceComments: false

IncludeBlocks: Preserve
IncludeCategories:
  - Regex: '.*'
    Priority: 1
IncludeIsMainRegex: '(Test)?$'
IndentCaseLabels: false
IndentGotoLabels: false


IndentWrappedFunctionNames: false
JavaScriptQuotes: Leave
JavaScriptWrapImports: true
KeepEmptyLinesAtTheStartOfBlocks: false
MacroBlockBegin: ''
MacroBlockEnd: ''
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
ObjCBinPackProtocolList: Auto
ObjCBlockIndentWidth: 8
ObjCSpaceAfterProperty: true
ObjCSpaceBeforeProtocolList: true

# Taken from git's rules
PenaltyBreakAssignment: 10
PenaltyBreakBeforeFirstCallParameter: 30
PenaltyBreakComment: 10
PenaltyBreakFirstLessLess: 0
PenaltyBreakString: 10
PenaltyExcessCharacter: 100
PenaltyReturnTypeOnItsOwnLine: 60

PointerAlignment: Right
ReflowComments: false
SortIncludes: false
SortUsingDeclarations: false
SpaceAfterCStyleCast: false
SpaceAfterTemplateKeyword: true
SpaceBeforeAssignmentOperators: true
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatementsExceptForEachMacros
SpaceBeforeRangeBasedForLoopColon: true
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 1
SpacesInAngles: false
SpacesInContainerLiterals: false
SpacesInCStyleCastParentheses: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
Standard: Cpp03

在各 project root 目录中创建:

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

生成 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 目录。
-target
bpf
-D__TARGET_ARCH_x86
-Wall
-Wextra
-I.
-I../headers
-idirafter
/Users/zhangjun/codes/ubuntu-5.15.0-75-headers
-g
-O2
-c

11.14 tempel
#

(use-package tempel
  :bind
  (("M-+" . tempel-complete)
   ("M-*" . tempel-insert))
  :init
  ;; 自定义模板文件。
  (setq tempel-path "/Users/alizj/emacs/templates")
  (defun tempel-setup-capf ()
    (setq-local completion-at-point-functions (cons #'tempel-expand completion-at-point-functions)))
  (add-hook 'conf-mode-hook 'tempel-setup-capf)
  (add-hook 'prog-mode-hook 'tempel-setup-capf)
  (add-hook 'text-mode-hook 'tempel-setup-capf)
  ;; 确保 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)

11.15 dape
#

C/C++/Rust 语言调试: llvm 项目提供了名为 lldb-vscode(新版被重命名为 lldb-dap) 的 debug adapter, 但相比 codelldb 功能较弱而且和 dape 不兼容(执行失败), 会报类似错误: failed to load objfile …lib/rustlib/x86_64-apple-darwin/lib/libcore-ef02792cbce15279.rlib, 所以需要使用 codelldb 来代替.

$ # 确保安装了 cmake 和 llvm 包且 /usr/local/opt/llvm/bin 位于 PATH 中, 参考前面的 clang 配置.

$ # 从 https://github.com/vadimcn/codelldb/releases 下载最新的 vsix 包
$ mkdir -p ~/.emacs.d/debug-adapters
$ unzip codelldb-<platform>-<os>.vsix -d ~/.emacs.d/debug-adapters/codelldb
$ # dape 后续使用安装的 codelldb 命令来进行调试(debug-adapters/codelldb/extension/adapter/codelldb)
$ ln -s ~/.emacs.d/debug-adapters/codelldb/extension/adapter/codelldb /usr/local/bin/codelldb

$ # 移除 codelldb 自带的 debugserver 而是使用 xcode-select --install 提供的版本, 否则会导致调试失败
  # 报错: Process exited with code -1. 参考:
  # https://github.com/eecs280staff/tutorials/issues/177#issue-2151798211
$ mv debug-adapters/codelldb/extension/lldb/bin/debugserver /tmp

Go 语言调试:

# https://github.com/go-delve/delve/tree/master/Documentation/installation
go install github.com/go-delve/delve/cmd/dlv@latest
sudo /usr/sbin/DevToolsSecurity -enable
sudo dscl . append /Groups/_developer GroupMembership $(whoami)

Python 语言调试:

pip install debugpy

配置 emacs dape:

(use-package dape
  ;; By default dape shares the same keybinding prefix as `gud'
  ;; If you do not want to use any prefix, set it to nil.
  ;; :preface
  ;; (setq dape-key-prefix "\C-x\C-a")
  ;;
  ;; May also need to set/change gud (gdb-mi) key prefix
  ;; (setq gud-key-prefix "\C-x\C-a")

  :hook
  ;; Save breakpoints on quit
  (kill-emacs . dape-breakpoint-save)
  ;; Load breakpoints on startup
  ;; (after-init . dape-breakpoint-load))

  :config
  (setq dape-buffer-window-arrangement 'right) ;; 'gud

  ;; To not display info and/or buffers on startup
  ;; (remove-hook 'dape-on-start-hooks 'dape-info)
  ;; (remove-hook 'dape-on-start-hooks 'dape-repl)

  ;; To display info and/or repl buffers on stopped
  ;; (add-hook 'dape-on-stopped-hooks 'dape-info)
  ;; (add-hook 'dape-on-stopped-hooks 'dape-repl)

  ;; Kill compile buffer on build success
  ;; (add-hook 'dape-compile-compile-hooks 'kill-buffer)

  ;; Save buffers on startup, useful for interpreted languages
  ;; (add-hook 'dape-on-start-hooks (lambda () (save-some-buffers t t)))
  )

查看和设置 dape-configs 变量: 例如 rust-ts-mode 使用的 codelldb-rust 配置如下:

  • 各参数可以使用 :key value 的形式来配置, 具体参数列表参考: codelldb/MANUAL.md
  • :request “launch” 表示 Launching a New Process, 支持的参数也在上面的 MANUAL.md 中有说明.
  • 通用配置参数:program/cargo/args/cwd/env/envFile/stdio/terminal/stopOnEntry;
(codelldb-rust
            modes (rust-mode rust-ts-mode)
		ensure dape-ensure-command
		command-cwd dape-command-cwd
		command "~/.emacs.d/debug-adapters/codelldb/extension/adapter/codelldb"  ;;以前安装的 codelldb 位置.
		command-args ("--port" :autoport "--settings" "{\"sourceLanguages\":[\"rust\"]}")
		port :autoport
		:type "lldb"
		:request "launch"
		:cwd "."
		:program (lambda nil ;; 要调试的二进制可执行程序
		  (file-name-concat "target" "debug"
				    (thread-first
				      (dape-cwd)
				      (directory-file-name)
				      (file-name-split)
				      (last)
				      (car))))
		:args [] ;; 传给可执行程序的参数列表
		:stopOnEntry nil)

其他配置:

(add-to-list 'dape-configs
    '(dlv
      modes (go-mode go-ts-mode)
      ensure dape-ensure-command
      command "dlv"
      command-args ("dap" "--listen" "127.0.0.1:5678")
      command-cwd dape-cwd-fn
      port 5678
      :request "launch"
      :type "debug"
      :cwd dape-cwd-fn
      :program dape-cwd-fn))

(setq dape-configs
    '((lldb-vscode modes (c-mode c-ts-mode c++-mode c++-ts-mode rust-mode rust-ts-mode)
                   command "lldb-vscode"
                   compile "make -j10"
                   :type "lldb-vscode" ensure dape-ensure-command
                   :cwd "xxx/build-dbg/xxx/src/"
                   :program "/usr/bin/python3"
                   :args ["EdgeCondTests.py"]
                   :MIMode "lldb"
                   :setupCommands [
                                   (
                                    :description "Enable pretty-printing for gdb"
                                    :text "-enable-pretty-printing"
                                    :ignoreFailures t
                                    )
                                   ]
                   )))

dape 不使用 launch.json 配置, 而是使用 emacs dir-local 变量, 例如:

;;; Directory Local Variables            -*- no-byte-compile: t -*-
;;; For more information see (info "(emacs) Directory Variables")

((c++-mode . ((dape-command . (codelldb-cc
			       command-cwd "~/C++/test/"
			       :program "main"
			       :args ["1"]
			       compile "g++ -g -o main main.cpp"))))) ;; 需要加 -g 来添加 debug flags

调试 dape: 设置 (setq dape-debug t), 然后查看 dape-repl, dape-connection events buffer 的内容.

调试程序:

  1. 在源码设置断点: M-x dape-breakpoint-toggle;
  2. 调试: M-x dape, Run adapter 后面输入调试命令:
    1. dape-configs 变量配置了各语言可以使用的 debug adapter 以及安装到的目录.

    2. 各语言的 debug adapter 默认别安装到 ~/.emacs.d/debug-adapters 目录下, 且需要和 dape-configs 配置的一致.

    3. 按 TAB 自动补全当前源码可以使用的 debug adapter, 同时显示对应的参数提示, 按 TAB 会自动补全, 例如调试 Rust 程序:

      codelldb-rust command-cwd "/Users/zhangjun/codes/rust/mydemo/" :program "target/debug/stream" :args ["1", "2"]
      
    4. 在 dape-repl 中输入 lldb 命令时需要加 ` 前缀, 比如 `help, `breakpoint list 等, lldb 命令参考: Tutorial;

  3. 结束调试: M-x dape-quit, 自动关闭 dape buffer 窗口.

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

;; xref 的 history 局限于当前窗口(默认全局)。
(setq xref-history-storage 'xref-window-local-history)
;; 快速在其他窗口查看定义。
(global-set-key (kbd "C-M-.") 'xref-find-definitions-other-window)

编译:

M-x compile
compile
C-x p c
project-compile

*compilation* buffer 快捷键:

TAB
compilation-next-error
RET
compile-goto-error
C-o
compilation-display-error
SPC
scroll-up-command
g
recompile
n
next-error-no-select
p
previous-error-no-select
<backtab>
compilation-previous-error
<follow-link>
mouse-face
M-n
compilation-next-error
M-p
compilation-previous-error
M-{
compilation-previous-file
M-}
compilation-next-file
C-c C-c
compile-goto-error
C-c C-f
next-error-follow-minor-mode # 浏览 error 时, 是否打开一个 buffer 来 follow error.
C-c C-k
kill-compilation

11.17 others
#

mwim: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))

expand-region 扩展区域:

(use-package expand-region
  :config
  (global-set-key (kbd "C-=") #'er/expand-region))

快速跳转到注释位置(自定义函数):

(defun my/goto-comment-start ()
  (interactive)
  (search-forward comment-start))
(define-key prog-mode-map (kbd "C-c C-;") 'my/goto-comment-start)

12 dired-sidebar
#

dired-sidebar 相比 treemacs/neotree 的优势是速度快, 而且复用 dired 的快捷键.

(use-package dired-sidebar
  :bind (("s-0" . dired-sidebar-toggle-sidebar))
  :ensure t
  :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 'ascii) ;;'icons 有问题, 不能显示.
  (setq dired-sidebar-use-term-integration t)
  (setq dired-sidebar-use-one-instance t)
  ;;(setq dired-sidebar-use-custom-font t)
  )

13 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-openai-key (auth-source-pick-first-password :host "jpaia.openai.azure.com"))
  (setq chatgpt-shell-chatgpt-streaming t)
  (setq chatgpt-shell-model-version "gpt-4-32k") ;; gpt-3.5-turbo gpt-4-32k
  (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)
  (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-path  "/openai/deployments/gpt-4-32k/chat/completions?api-version=2024-02-15-preview")
  (setq chatgpt-shell-api-url-base "https://jpaia.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 "jpaia.openai.azure.com")))))

14 terminal
#

安装 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) ;; 不显示行号。
		  ;;(font-lock-mode -1) ;; 不显示字体颜色。
		  ;;(yas-minor-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 "s-i") 'vterm-toggle-cd-show)
  (define-key vterm-mode-map (kbd "s-n") 'vterm-toggle-forward)
  (define-key vterm-mode-map (kbd "s-p") 'vterm-toggle-backward)
  (define-key vterm-copy-mode-map (kbd "s-i") 'vterm-toggle-cd-show)
  (define-key vterm-copy-mode-map (kbd "s-n") 'vterm-toggle-forward)
  (define-key vterm-copy-mode-map (kbd "s-p") 'vterm-toggle-backward))

vterm-extra 提供了 vterm buffer 命令行编辑的能力,结束后按 C-c C-c 自动粘贴到对应的 vterm 中。

(use-package vterm-extra
  :vc (:fetcher github :repo 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)

;; 在当前窗口右侧拆分出两个子窗口并固定,分别为一个 eshell 和当前 buffer 。
(defun my/split-windows()
  "Split windows my way."
  (interactive)
  (split-window-right 150)
  (other-window 1)
  (split-window-below)
  (eshell)
  (other-window -1)
  ;; never open any buffer in window with shell
  (set-window-dedicated-p (nth 1 (window-list)) t)
  (set-window-dedicated-p (nth 2 (window-list)) t))
(global-set-key (kbd "C-s-`") 'my/split-windows)

;; 在当前 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)

tramp:

(use-package tramp
  :config
  ;; 使用远程主机自己的 PATH(默认是本地的 PATH)
  (setq tramp-remote-path '(tramp-default-remote-path "/bin" "/usr/bin" "/sbin" "/usr/sbin" "/usr/local/bin" "/usr/local/sbin"))
  ;;(add-to-list 'tramp-remote-path 'tramp-own-remote-path)
  ;; 使用 ~/.ssh/config 中的 ssh 持久化配置。(Emacs 默认复用连接,但不持久化连接)
  (setq tramp-use-ssh-controlmaster-options nil)
  (setq  tramp-ssh-controlmaster-options nil)
  ;; TRAMP buffers 关闭 version control, 防止卡住。
  (setq vc-ignore-dir-regexp (format "\\(%s\\)\\|\\(%s\\)" vc-ignore-dir-regexp tramp-file-name-regexp))
  ;; 关闭自动保存 ad-hoc proxy 代理配置, 防止为相同 IP 的 VM 配置了错误的 Proxy.
  (setq tramp-save-ad-hoc-proxies nil)
  ;; 调大远程文件名过期时间(默认 10s), 提高查找远程文件性能.
  (setq remote-file-name-inhibit-cache 1800)
  ;; 设置 tramp-verbose 10 打印详细信息。
  (setq tramp-verbose 1)
  ;; 增加压缩传输的文件起始大小(默认 4KB),否则容易出错: “gzip: (stdin): unexpected end of file”
  (setq tramp-inline-compress-start-size (* 1024 8))
  ;; 当文件大小超过 tramp-copy-size-limit 时,用 external methods(如 scp)来传输,从而大大提高拷贝效率。
  (setq tramp-copy-size-limit (* 1024 100))
  (setq tramp-allow-unsafe-temporary-files t)
  ;; 本地不保存 tramp 备份文件。
  (setq tramp-backup-directory-alist `((".*" .  nil)))
  ;; Backup (file~) disabled and auto-save (#file#) locally to prevent delays in editing remote files
  ;; https://stackoverflow.com/a/22077775
  (add-to-list 'backup-directory-alist (cons tramp-file-name-regexp nil))
  ;; 临时目录中保存 TRAMP auto-save 文件, 重启后清空,防止启动时 tramp 扫描文件卡住。
  (setq tramp-auto-save-directory temporary-file-directory)
  ;; 连接历史文件。
  (setq tramp-persistency-file-name (expand-file-name "tramp-connection-history" user-emacs-directory))
  ;; 避免在 shell history 中添加过多 vterm 自动执行的命令。
  (setq tramp-histfile-override nil)
  ;; 在整个 Emacs session 期间保存 SSH 密码.
  (setq password-cache-expiry nil)
  (setq tramp-default-method "ssh")
  (setq tramp-default-remote-shell "/bin/bash")
  (setq tramp-encoding-shell "/bin/bash")
  (setq tramp-default-user "root")
  (setq tramp-terminal-type "tramp")
  (customize-set-variable 'tramp-encoding-shell "/bin/bash")
  (add-to-list 'tramp-connection-properties '("/ssh:" "remote-shell" "/bin/bash"))
  (setq tramp-connection-local-default-shell-variables
        '((shell-file-name . "/bin/bash")
          (shell-command-switch . "-c")))

  ;; 自定义远程环境变量。
  (let ((process-environment tramp-remote-process-environment))
    ;; 设置远程环境变量 VTERM_TRAMP, 远程机器的 emacs_bashrc 根据这个变量设置 VTERM 参数。
    (setenv "VTERM_TRAMP" "true")
    (setq tramp-remote-process-environment process-environment)))

;; 切换 Buffer 时设置 VTERM_HOSTNAME 环境变量为多跳的最后一个主机名,并通过 vterm-environment 传递到
;; 远程 vterm shell 环境变量中,这样远程机器 ~/.bashrc 读取并执行的 emacs_bashrc 脚本正确设置 Buffer
;; 名称和 vtem_prompt_end 函数, 从而确保目录跟踪功能正常,以及通过主机名而非 IP 来打开远程 vterm
;; shell, 确保 SSH ProxyJump 功能正常(只能通过主机名而非 IP 访问),以及避免目标 IP 重复时连接复用
;; 错误的问题。
(defvar my/remote-host "")
(add-hook 'buffer-list-update-hook
          (lambda ()
            (when (file-remote-p default-directory)
              (setq my/remote-host (file-remote-p default-directory 'host))
              ;; 动态计算 ENV=VALUE.
              (require 'vterm)
              (setq vterm-environment `(,(concat "VTERM_HOSTNAME=" my/remote-host))))))

(use-package consult-tramp
  :vc (:fetcher github :repo Ladicle/consult-tramp)
  :custom
  ;; 默认为 scpx 模式,不支持 SSH 多跳 Jump。
  (consult-tramp-method "ssh")
  ;; 打开远程的 /root 目录,而非 ~, 避免 tramp hang。
  ;; https://lists.gnu.org/archive/html/bug-gnu-emacs/2007-07/msg00006.html
  (consult-tramp-path "/root/")
  ;; 即使 ~/.ssh/config 正确 Include 了 hosts 文件,这里还是需要配置,因为 consult-tramp 不会解析 Include 配置。
  (consult-tramp-ssh-config "~/work/proxylist/hosts_config"))
  • tramp-default-method 缺省值为 scp, 不支持多跳(但拷贝大文件时性能更高),再打开多跳远程文件时每次都需要修改/- 中的 -为 ssh,较麻烦,所以设置为 ssh。
  • tramp 打开远程文件时,避免使用 ~ 路径,而应该是绝对路径, 防止切换 buffer 时卡住;
  • 修改 net/tramp-sh.el 中的 tramp-send-commad, 将 (concat “exec env TERM=’%s’ INSIDE_EMACS=’%s’ " “ENV=%s %s PROMPT_COMMAND=’’ PS1=%s PS2=’’ PS3=’’ %s %s”) 中最后的 “-i” 去掉, 然后删除同目录下的 tramp-sh.elc 文件;

15 others
#

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

;; 在另一个 panel buffer 中展示按键。
(use-package command-log-mode :commands command-log-mode)

(use-package hydra :commands defhydra)

历史记录:

(use-package recentf
  :config
  (setq recentf-save-file "~/.emacs.d/recentf")
  ;; 不自动清理 recentf 记录。
  (setq recentf-auto-cleanup 'never)
  ;; 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 100)
  (setq recentf-max-saved-items 200) ;; default 20
  ;; recentf-exclude 的参数是正则表达式列表,不支持 ~ 引用家目录。
  ;; emacs-dashboard 不显示这里排除的文件。
  (setq recentf-exclude `(,(recentf-expand-file-name "~\\(straight\\|ln-cache\\|etc\\|var\\|.cache\\|backup\\|elfeed\\)/.*")
                          ,(recentf-expand-file-name "~\\(recentf\\|bookmarks\\|archived.org\\)")
                          ,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/.*"
                          ,(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)

自动备份:

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

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

Emacs 29 xwidget-webkit 对 Mac 支持不好( Better support for xwidget-webkit), 部分功能只有 GTK/X11才支持, 如: buffer 内搜索 increase-search/webkit-history:

  • 如果要复制 xwidget 的内容,需要选择后右击,从上下文菜单中选择 copy。
(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)

在线文档和翻译:

;;在线搜索, 可以先选中 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))

16 refs
#

ANKI_NOTE_HASH: 64901ec4f38ae35f38b42523c75cb9ea

ANKI_NOTE_ID: 1703514360198

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

  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
emacs - 这篇文章属于一个系列。
§ 1: 本文

相关文章

Emacs 个人参考手册
·50513 字
emacs emacs
使用 hugo 和 ox-hugo 写博客
··7859 字
emacs hugo org-mode blog

这篇文章总结下我使用 hugo 和 blowfish 主题搭建博客的配置参数,同时也记录了使用 org-mode 和 ox-hugo 来写博客的过程。

Linux 内核追踪和 eBPF 介绍
··8046 字
ebpf ebpf

eBPF 是当今热门的底层技术,在网络、安全、可观测性、云原生等场景得到广泛应用。

本文档先介绍 Linux 内核的各种追踪技术,让大家对于各种事件源、内核各种追踪框架、用户工具等有个初步了解,然后介绍 eBPF 的发展历程、开发和执行流程、开发框架选择和 Demo 示例,希望对于想了解 Linux 内核追踪和 eBPF 技术的同学有所帮助。

Function Stack Unwinding
··5270 字
debug linux dwarf debug

介绍 Linux 函数调用栈生成和管理机制。