morole / emacs

Make emacs powerful forever!

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

menu

install

编译安装 Emacs 28:

# sudo xcode-select --reset
# xcode-select --install
# brew reinstall gcc libgccjit
# brew uninstall emacs-plus@28
brew install emacs-plus@28 --HEAD --with-no-titlebar --with-no-frame-refocus --with-xwidgets --with-native-comp --with-imagemagick --with-nobu417-big-sur-icon
brew link --overwrite emacs-plus@28
ln -sf /usr/local/opt/emacs-plus@28/Emacs.app /Applications

安装命令工具:

brew install npm nvm
npm config set registry=http://registry.npm.taobao.org

init

early-init.elEmacs 启动时最开始执行的文件(在加载包和初始化 UI 之前),执行复杂逻辑可能导致 Emacs 启动时悄 无声息失败,所以该文件尽量以变量定义为主。

;; Emacs 28
(when (fboundp 'native-compile-async)
  (setenv "LIBRARY_PATH"
          (concat (getenv "LIBRARY_PATH")
                  "/usr/local/opt/gcc/lib/gcc/11:/usr/local/opt/gcc/lib/gcc/11/gcc/x86_64-apple-darwin21/11"))
  (setq native-comp-speed 2)
  (setq native-comp-async-jobs-number 4)
  (setq native-comp-deferred-compilation nil)
  (setq native-comp-deferred-compilation-deny-list '())
  (setq native-comp-async-report-warnings-errors 'silent))

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

;; 关闭 cl 告警。
(setq byte-compile-warnings '(cl-functions))

;; 关闭 package.el(后续使用 straight.el) 。
(setq package-enable-at-startup nil)

;; 启动时开启 debug, 启动后关闭。
(setq debug-on-error t)
(add-hook 'emacs-startup-hook (lambda () (setq debug-on-error nil)))

;; 设置缩放模式, 避免 MacOS 最大化窗口后右边和下边有空隙。
(setq frame-inhibit-implied-resize t)
(setq frame-resize-pixelwise t)

;; 加 t 参数让 togg-frame-XX 最后运行,这样最大化才生效。
;;(add-hook 'window-setup-hook 'toggle-frame-fullscreen t) 
(add-hook 'window-setup-hook 'toggle-frame-maximized 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))))

;; 个人信息。
(setq user-full-name "zhangjun")
(setq user-mail-address "geekard@qq.com")

;; 缺省使用 email 地址加密。
(setq-default epa-file-select-keys nil)
(setq-default epa-file-encrypt-to user-mail-address)

;; 使用 minibuffer 输入 GPG 密码。
(setq-default epa-pinentry-mode 'loopback)

;; 加密认证信息文件。
(setq auth-sources '("~/.authinfo.gpg"))

;; 缓存对称加密密码。
(setq epa-file-cache-passphrase-for-symmetric-encryption t)

;; 认证不过期, 默认 7200。
(setq auth-source-cache-expiry nil)
;;(setq auth-source-debug t)

package

配置软件源:

(require 'package)
(setq package-archives '(("elpa" . "https://elpa.gnu.org/packages/")
                         ("melpa" . "https://melpa.org/packages/")))

使用 use-package+straight 替代 Emacs package.el, 它用 Git Checkout+Build 机制安装软件包(而非直接从软件源下载):

;; 配置 use-package 默认使用 straight 安装包。
(setq straight-use-package-by-default t)
(setq straight-vc-git-default-clone-depth 1)
(setq straight-recipes-gnu-elpa-use-mirror t)
(setq straight-check-for-modifications '(check-on-save find-when-checking watch-files))
(setq straight-host-usernames '((github . "opsnull")))

;; 安装 straight.el 。
(defvar bootstrap-version)
(let ((bootstrap-file
       (expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory))
      (bootstrap-version 5))
  (unless (file-exists-p bootstrap-file)
    (with-current-buffer
        (url-retrieve-synchronously
         "https://raw.githubusercontent.com/raxod502/straight.el/develop/install.el"
         'silent 'inhibit-cookies)
      (goto-char (point-max))
      (eval-print-last-sexp)))
  (load bootstrap-file nil 'nomessage))

;; 安装 use-package 。
(straight-use-package 'use-package)
(setq use-package-verbose t)
(setq use-package-compute-statistics t)

;; 为 use-package 添加 :ensure-system-package 指令。
(use-package use-package-ensure-system-package)

exec-path-from-shellShell 环境变量拷贝到 ~Emacs~:

(use-package exec-path-from-shell
  :demand
  :custom
  ;; 去掉 -i 参数, 加快启动速度。
  (exec-path-from-shell-arguments '("-l")) 
  (exec-path-from-shell-check-startup-files nil)
  (exec-path-from-shell-variables '("PATH" "MANPATH" "GOPATH" "GOPROXY" "GOPRIVATE" "GOFLAGS" "GO111MODULE"))
  :config
  (when (memq window-system '(mac ns x))
    (exec-path-from-shell-initialize)))

tuning

性能调优: 参考 doom core.el

;; 提升 IO 性能。
(setq process-adaptive-read-buffering nil)
;; 增加单次读取进程输出的数据量(缺省 4KB) 。
(setq read-process-output-max (* 1024 1024))

;; 提升长行处理性能。
(setq bidi-inhibit-bpa t)
(setq-default bidi-display-reordering 'left-to-right)
(setq-default bidi-paragraph-direction 'left-to-right)

;; 缩短 fontify 时间。
(setq jit-lock-defer-time nil)
(setq jit-lock-context-time 0.1)
;; 更积极的 fontify 。
(setq fast-but-imprecise-scrolling nil)
(setq redisplay-skip-fontification-on-input nil)

;; 缩短更新 screen 的时间。
(setq idle-update-delay 0.1)

;; 使用字体缓存,避免卡顿。
(setq inhibit-compacting-font-caches t)

;; Garbage Collector Magic Hack
(use-package gcmh
  :demand
  :init
  ;; 在 minibuffer 显示 GC 信息。
  ;;(setq garbage-collection-messages t)
  ;;(setq gcmh-verbose t)
  (setq gcmh-idle-delay 5)
  (setq gcmh-high-cons-threshold (* 64 1024 1024))
  (gcmh-mode 1)
  (gcmh-set-high-threshold))

face

(when (memq window-system '(mac ns x))
  ;; 关闭各种图形元素。
  (tool-bar-mode -1)
  (scroll-bar-mode -1)
  (menu-bar-mode -1)
  ;; 使用更瘦字体。
  (setq ns-use-thin-smoothing t)
  ;; 不在新 frame 打开文件(如 Finder 的 "Open with Emacs") 。
  (setq ns-pop-up-frames nil)
  ;; 一次滚动一行,避免窗口跳动。
  (setq mouse-wheel-scroll-amount '(1 ((shift) . hscroll)))
  (setq mouse-wheel-scroll-amount-horizontal 1)
  (setq mouse-wheel-follow-mouse t)
  (setq mouse-wheel-progressive-speed nil)
  (xterm-mouse-mode t))

;; 关闭启动消息。
(setq inhibit-startup-screen t)
(setq inhibit-startup-message t)
(setq inhibit-startup-echo-area-message t)
(setq initial-scratch-message nil)

;; 指针闪动。
(blink-cursor-mode 1)

;; 调大 fringe, 避免行号列跳动。
(set-fringe-mode 10)

;; 出错提示。
(setq visible-bell t)

;; 关闭对话框。
(setq use-file-dialog nil)
(setq use-dialog-box nil)

;; 窗口间显示分割线。
(setq window-divider-default-places t)
(add-hook 'window-setup-hook #'window-divider-mode)

;; 左右分屏, nil: 上下分屏。
(setq split-width-threshold 30)

;; 复用当前 frame 。
(setq display-buffer-reuse-frames t)

;; 滚动一屏后, 显示 3 行上下文。
(setq next-screen-context-lines 3)

;; 平滑地进行半屏滚动,避免滚动后 recenter 操作。
(setq scroll-step 1)
(setq scroll-conservatively 10000)
(setq scroll-margin 2)

;; 滚动时保持光标位置。
(setq scroll-preserve-screen-position 1)

;; 像素平滑滚动(Emacs 29 开始支持)。
(if (boundp 'pixel-scroll-precision-mode)
    (pixel-scroll-precision-mode t))

;; 关闭 mouse-wheel-text-scale 快捷键 (容易触碰误操作) 。
(global-unset-key (kbd "C-<wheel-down>"))
(global-unset-key (kbd "C-<wheel-up>"))

;; 大文件不显示行号。
(setq large-file-warning-threshold nil)
(setq line-number-display-limit large-file-warning-threshold)
(setq line-number-display-limit-width 1000)
(dolist (mode '(text-mode-hook prog-mode-hook conf-mode-hook))
  (add-hook mode (lambda () (display-line-numbers-mode 1))))

;; 根据窗口自适应显示图片。
(setq image-transform-resize t)
(auto-image-file-mode t)

;; 显示缩进。
(use-package highlight-indent-guides
  :custom
  (highlight-indent-guides-method 'character)
  (highlight-indent-guides-responsive 'top)
  (highlight-indent-guides-suppress-auto-error t)
  (highlight-indent-guides-delay 0.1)
  :config
  (add-hook 'python-mode-hook 'highlight-indent-guides-mode)
  (add-hook 'yaml-mode-hook 'highlight-indent-guides-mode)
  (add-hook 'js-mode-hook 'highlight-indent-guides-mode)
  (add-hook 'web-mode-hook 'highlight-indent-guides-mode))

;; 预览主题: https://emacsthemes.com/
(use-package doom-themes
  :demand
  ;; 添加 "extensions/*" 后才支持 visual-bell/treemacs/org 配置。
  :straight (:files ("*.el" "themes/*" "extensions/*"))
  :custom-face
  (doom-modeline-buffer-file ((t (:inherit (mode-line bold)))))
  :custom
  (doom-themes-enable-bold t)
  (doom-themes-enable-italic t)
  (doom-themes-treemacs-theme "doom-colors")
  ;; modeline 两边各加 4px 空白。
  (doom-themes-padded-modeline t)
  :config
  (doom-themes-visual-bell-config)
  (load-theme 'doom-palenight t)
  ;; 为 treemacs 关闭 variable-pitch 模式,否则显示的较丑!
  ;; 必须在执行 doom-themes-treemacs-config 前设置该变量为 nil, 否则不生效。
  (setq doom-themes-treemacs-enable-variable-pitch nil)
  (doom-themes-treemacs-config)
  (doom-themes-org-config))

;; modeline 显示电池和日期时间。
(display-battery-mode t)
(column-number-mode t)
(size-indication-mode -1)
(display-time-mode t)
(setq display-time-24hr-format t)
(setq display-time-default-load-average nil)
(setq display-time-load-average-threshold 5)
(setq display-time-format "%m/%dT%H:%M")
(setq display-time-day-and-date t)
(setq indicate-buffer-boundaries (quote left))

;; 后面将 mode-line face 的字体设置为 Fira Code Retina, 兼容性比 Sarasa Mono SC 好, 不会溢出。
(use-package doom-modeline
  :demand
  :custom
  ;; 不显示换行和编码(节省空间)。
  (doom-modeline-buffer-encoding nil)
  ;; 显示语言版本。
  (doom-modeline-env-version t)
  ;; 不显示 Go 版本。
  (doom-modeline-env-enable-go nil)
  (doom-modeline-unicode-fallback t)
  ;; 不显示 project 名称。
  ;;(doom-modeline-project-detection nil)
  ;; 不显示文件所属项目,否则 TRAMP 变慢:https://github.com/seagle0128/doom-modeline/issues/32
  ;;(doom-modeline-buffer-file-name-style 'file-name)
  (doom-modeline-buffer-file-name-style 'relative-from-project)
  (doom-modeline-vcs-max-length 30)
  (doom-modeline-github nil)
  (doom-modeline-height 2)
  :init
  (doom-modeline-mode 1))

;; dashboard 不能 :demand 启动, 否则会导致 mode-line 溢出。
(use-package dashboard
  ;; Emacs 启动完成后再启动 dashboard。
  :hook (after-init . dashboard-setup-startup-hook)
  :config
  (setq dashboard-banner-logo-title "Happy Hacking & Writing 🎯")
  ;;(setq dashboard-projects-backend #'projectile)
  (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-items '((recents . 10) (projects . 8) (agenda . 3)))) 

(use-package centaur-tabs
  :hook (emacs-startup . centaur-tabs-mode)
  :init
  (setq centaur-tabs-set-icons t)
  (setq centaur-tabs-height 25)
  (setq centaur-tabs-gray-out-icons 'buffer)
  (setq centaur-tabs-set-modified-marker t)
  (setq centaur-tabs-cycle-scope 'tabs)
  (setq centaur-tabs-enable-ido-completion nil)
  (setq centaur-tabs-set-bar 'under)
  (setq x-underline-at-descent-line t)
  (setq centaur-tabs-show-navigation-buttons t)
  (setq centaur-tabs-enable-key-bindings t)
  :config
  (centaur-tabs-mode t)
  (centaur-tabs-headline-match)
  (centaur-tabs-enable-buffer-reordering)
  (centaur-tabs-group-buffer-groups)
  ;;(centaur-tabs-group-by-projectile-project)
  (defun centaur-tabs-hide-tab (x)
    (let ((name (format "%s" x)))
      (or
       (window-dedicated-p (selected-window))
       ;; 不显示以 * 开头的 buffer 。
       (string-prefix-p "*" name)
       (and (string-prefix-p "magit" name)
            (not (file-name-extension name)))))))

;; 显示光标位置。
(use-package beacon
  :config
  ;; 翻页时不高亮位置。
  (setq beacon-blink-when-window-scrolls nil)
  (setq beacon-blink-duration 0.3)
  (beacon-mode 1))

;; 切换透明背景。
(defun my/toggle-transparency ()
  (interactive)
  (set-frame-parameter (selected-frame) 'alpha '(90 . 90))
  (add-to-list 'default-frame-alist '(alpha . (90 . 90))))

;; 在 frame 底部显示窗口。
(setq display-buffer-alist
      `((,(rx bos (or "*Apropos*" "*Help*" "*helpful" "*info*" "*Summary*" "*lsp-help*" "*vterm") (0+ not-newline))
         (display-buffer-reuse-mode-window display-buffer-below-selected)
         (window-height . 0.43)
         (mode apropos-mode help-mode helpful-mode Info-mode Man-mode))))

font

;; 参考: https://github.com/DogLooksGood/dogEmacs/blob/master/elisp/init-font.el
;; 缺省字体(英文,如显示代码)。
(setq +font-family "Fira Code Retina")
;; modeline 设置为 Fira Code Retina, 兼容性比 Sarasa Mono SC 好, 不会溢出。
(setq +modeline-font-family "Fira Code Retina")
;; 其它均使用 Sarasa Mono SC 字体。
(setq +fixed-pitch-family "Sarasa Mono SC")
(setq +variable-pitch-family "Sarasa Mono SC")
(setq +font-unicode-family "Sarasa Mono SC")
(setq +font-size 14)

;; 设置缺省字体。
(defun +load-base-font ()
  ;; 只为缺省字体设置 size, 其它字体都通过 :height 动态伸缩。
  (let* ((font-spec (format "%s-%d" +font-family +font-size)))
    (set-frame-parameter nil 'font font-spec)
    (add-to-list 'default-frame-alist `(font . ,font-spec))))

;; 设置各特定 face 的字体。
(defun +load-face-font (&optional frame)
  (let ((font-spec (format "%s" +font-family))
        (modeline-font-spec (format "%s" +modeline-font-family))
        (variable-pitch-font-spec (format "%s" +variable-pitch-family))
        (fixed-pitch-font-spec (format "%s" +fixed-pitch-family)))
    (set-face-attribute 'variable-pitch frame :font variable-pitch-font-spec :height 1.2)
    (set-face-attribute 'fixed-pitch frame :font fixed-pitch-font-spec :height 1.0)
    (set-face-attribute 'fixed-pitch-serif frame :font fixed-pitch-font-spec :height 1.0)
    (set-face-attribute 'tab-bar frame :font font-spec :height 1.0)
    (set-face-attribute 'mode-line frame :font modeline-font-spec :height 1.0)
    (set-face-attribute 'mode-line-inactive frame :font modeline-font-spec :height 1.0)))

;; 设置中文字体。
(defun +load-ext-font ()
  (when window-system
    (let ((font (frame-parameter nil 'font))
          (font-spec (font-spec :family +font-unicode-family)))
      (dolist (charset '(kana han hangul cjk-misc bopomofo symbol))
        (set-fontset-font font charset font-spec)))))

;; 设置 Emoji 字体。
(defun +load-emoji-font ()
  (when window-system
    (setq use-default-font-for-symbols nil)
    (set-fontset-font t '(#x1f000 . #x1faff) (font-spec :family "Apple Color Emoji"))
    (set-fontset-font t 'symbol (font-spec :family "Symbola"))))

(add-hook 'after-make-frame-functions 
          ( lambda (f) 
            (+load-face-font f)
            (+load-ext-font)
            (+load-emoji-font)))

(defun +load-font ()
  (+load-base-font)
  (+load-face-font)
  (+load-ext-font)
  (+load-emoji-font))

(+load-font)

;; all-the-icons 和 fire-code-mode 只能在 GUI 模式下使用。
(when (display-graphic-p)
  (use-package all-the-icons
    :demand)
  
  (use-package fira-code-mode
    :custom
    (fira-code-mode-disabled-ligatures '("[]" "#{" "#(" "#_" "#_(" "x"))
    :hook prog-mode))
  • 查看 Emacs 支持的字体名称: (print (font-family-list))
  • 安装、更新 FiraCode Symbol 字体: M-x fira-code-mode-install-fonts
  • 安装、更新 Icon 字体: M-x all-the-icons-install-fonts, 包括 file-icons/fontawesome/octicons/weathericons/material-design 字体。

completion

vertico

(use-package vertico
  :demand
  :straight (:repo "minad/vertico" :files ("*" "extensions/*.el" (:exclude ".git")))
  :bind
  (:map vertico-map
        ;; 在多个 source 中切换(如 consult-buffer, consult-grep) 。
        ("C-M-n" . vertico-next-group)
        ("C-M-p" . vertico-previous-group)
        ;; 快速插入。
        ("M-i" . vertico-quick-insert)
        ("M-e" . vertico-quick-exit)
        ;; 文件路径操作。
        ("<backspace>" . vertico-directory-delete-char)
        ("C-w" . vertico-directory-delete-word)
        ("C-<backspace>" . vertico-directory-delete-word)
        ("RET" . vertico-directory-enter))
  :hook
  (
   ;; 在输入时清理文件路径。
   (rfn-eshadow-update-overlay . vertico-directory-tidy)
   ;; 确保 vertico 状态被保存(用于支持 vertico-repeat)。
   (minibuffer-setup . vertico-repeat-save))
  :config
  (setq vertico-count 20)
  (setq vertico-cycle nil)
  (vertico-mode 1)

  ;; 重复上一次 vertico session;
  (global-set-key "\M-r" #'vertico-repeat-last)
  (global-set-key "\M-R" #'vertico-repeat-select)

  ;; 开启 vertico-multiform, 为 commands 或 categories 设置不同的显示风格 。
  (vertico-multiform-mode)

  ;; 设置命令显示风格。
  (setq vertico-multiform-commands
        ;; 参数是 vertico-<name>-mode 中的 <name>, 可以多个联合使用 。
        ;; 在单独 buffer 中显示结果 consult-imenu 结果。
        '((consult-imenu buffer)
          (consult-line buffer)
          (consult-mark buffer)
          (consult-global-mark buffer)
          (consult-find buffer)))

  ;; 按照 completion category 设置显示风格, 优先级比 vertico-multiform-commands 低。
  ;; 为 file 设置 grid 模式, 为 grep 设置 buffer 模式.
  (setq vertico-multiform-categories
        '((file grid)
          (consult-grep buffer))))

(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)
  (setq read-extended-command-predicate #'command-completion-default-include-p)
  ;; 开启 minibuffer 递归编辑。
  (setq enable-recursive-minibuffers t))

orderless

使用 orderless 过滤候选者, 支持多种 dispatch 组合, 如 !zhangjun hang$:

  • ~~flex flex~~
  • =literal literal=
  • %char-fold char-fold%
  • `initialism initialism`
  • !without-literal
  • .ext
  • regexp$
(use-package orderless
  :demand
  :config
  (defvar +orderless-dispatch-alist
    '((?% . char-fold-to-regexp)
      (?! . orderless-without-literal)
      (?`. orderless-initialism)
      (?= . orderless-literal)
      (?~ . orderless-flex)))

  (defun +orderless-dispatch (pattern index _total)
    (cond
     ((string-suffix-p "$" pattern)
      `(orderless-regexp . ,(concat (substring pattern 0 -1) "[\x100000-\x10FFFD]*$")))
     ;; 文件扩展。
     ((and
       ;; 补全文件名或 eshell.
       (or minibuffer-completing-file-name
           (derived-mode-p 'eshell-mode))
       ;; 文件名扩展
       (string-match-p "\\`\\.." pattern))
      `(orderless-regexp . ,(concat "\\." (substring pattern 1) "[\x100000-\x10FFFD]*$")))
     ;; 忽略单个 !
     ((string= "!" pattern) `(orderless-literal . ""))
     ;; 前缀和后缀。
     ((if-let (x (assq (aref pattern 0) +orderless-dispatch-alist))
          (cons (cdr x) (substring pattern 1))
        (when-let (x (assq (aref pattern (1- (length pattern))) +orderless-dispatch-alist))
          (cons (cdr x) (substring pattern 0 -1)))))))

  ;; 自定义 orderless 风格。
  (orderless-define-completion-style +orderless-with-initialism
    (orderless-matching-styles '(orderless-initialism orderless-literal orderless-regexp)))

  (setq completion-styles '(orderless)
        completion-category-defaults nil
        completion-category-overrides '((buffer (styles basic partial-completion))
                                        (file (styles basic partial-completion))
                                        (command (styles +orderless-with-initialism))
                                        (variable (styles +orderless-with-initialism))
                                        (symbol (styles +orderless-with-initialism)))
        ;; 使用 SPACE 来分割过滤字符串, SPACE 可以用 \ 转义。
        orderless-component-separator #'orderless-escapable-split-on-space
        orderless-style-dispatchers '(+orderless-dispatch)))

consult

(use-package consult
  :ensure-system-package (rg . ripgrep)
  :demand
  :bind
  (;; C-c 绑定 (mode-specific-map)
   ("C-c h" . consult-history)
   ("C-c m" . consult-mode-command)
   ;; C-x 绑定 (ctl-x-map)
   ("C-M-:" . consult-complex-command)
   ("C-x b" . consult-buffer)
   ("C-x 4 b" . consult-buffer-other-window)
   ("C-x 5 b" . consult-buffer-other-frame)
   ("C-x r b" . consult-bookmark)
   ;; 寄存器绑定。
   ("M-#" . consult-register-load)
   ("M-'" . consult-register-store)
   ("C-M-#" . consult-register)
   ;; 其它自定义绑定。
   ("M-y" . consult-yank-pop)
   ("<help> a" . consult-apropos)
   ;; M-g 绑定 (goto-map)
   ("M-g e" . consult-compile-error)
   ("M-g f" . consult-flycheck)
   ("M-g g" . consult-goto-line)
   ("M-g M-g" . consult-goto-line)
   ("M-g o" . consult-outline)
   ("M-g m" . consult-mark)
   ("M-g k" . consult-global-mark)
   ("M-g i" . consult-imenu)
   ("M-g I" . consult-imenu)
   ;; M-s 绑定 (search-map)
   ("M-s d" . consult-find)
   ("M-s D" . consult-locate)
   ("M-s g" . consult-grep)
   ("M-s G" . consult-git-grep)
   ("M-s r" . consult-ripgrep)
   ("M-s l" . consult-line)
   ("M-s L" . consult-line-multi)
   ("M-s m" . consult-multi-occur)
   ("M-s k" . consult-keep-lines)
   ("M-s u" . consult-focus-lines)
   ;; Isearch 集成。
   ("M-s e" . consult-isearch-history)
   :map isearch-mode-map
   ("M-e" . consult-isearch-history)
   ("M-s e" . consult-isearch-history)
   ("M-s l" . consult-line)
   ("M-s L" . consult-line-multi)
   ;; Minibuffer 历史。
   :map minibuffer-local-map
   ("M-s" . consult-history)
   ("M-r" . consult-history))
  :hook
  (completion-list-mode . consult-preview-at-point-mode)
  :init
  ;; 如果搜索字符少于 3,可以添加后缀#开始搜索,如 #gr#。
  (setq consult-async-min-input 3)
  (setq consult-async-input-debounce 0.4)
  (setq consult-async-input-throttle 0.5)
  ;; 预览寄存器。
  (setq register-preview-delay 0.1)
  (setq register-preview-function #'consult-register-format)
  (advice-add #'register-preview :override #'consult-register-window)
  ;; 支持使用 Enter 来选择、反选候选项(例如 consult-multi-occur 场景)。
  (advice-add #'completing-read-multiple :override #'consult-completing-read-multiple)
  (setq xref-show-xrefs-function #'consult-xref)
  (setq xref-show-definitions-function #'consult-xref)
  :config
  ;; 按 C-l 激活预览,否则 buffer 列表中有大文件或远程文件时会卡住。
  (setq consult-preview-key (kbd "C-l"))
  (setq consult-narrow-key "<")
  (define-key consult-narrow-map (vconcat consult-narrow-key "?") #'consult-narrow-help)
  (setq completion-in-region-function #'consult-completion-in-region)
  ;; projectile 集成(缺省使用 project.el) 。
  ;;(autoload 'projectile-project-root "projectile")
  ;;(setq consult-project-function 'projectile-project-root)

  ;; 多选时按键绑定(例如 consult-multi-occur 场景)。
  ;; TAB - Select/deselect, RET - 提交和退出。
  (define-key consult-crm-map "\t" #'vertico-exit)
  (define-key consult-crm-map "\r" #'+vertico-crm-exit)
  (defun +vertico-crm-exit ()
    (interactive)
    (run-at-time 0 nil #'vertico-exit)
    (funcall #'vertico-exit))

  ;; 不对 consult-line 结果进行排序(按行号排序)。
  (consult-customize consult-line :prompt "Search: " :sort nil))

(use-package consult-project-extra
  :straight (consult-project-extra :type git :host github :repo "Qkessler/consult-project-extra")
  :bind
  (("C-c p f" . consult-project-extra-find)
   ("C-c p o" . consult-project-extra-find-other-window)))

(use-package consult-dir
  :bind
  (("C-x C-d" . consult-dir)
   :map minibuffer-local-completion-map
   ("C-x C-d" . consult-dir)
   ("C-x C-j" . consult-dir-jump-file)))

embark

(use-package embark
  :init
  ;; 使用 C-h 来显示 key preifx 绑定。
  (setq prefix-help-command #'embark-prefix-help-command)
  :config
  (setq embark-prompter 'embark-keymap-prompter)
  (setq embark-collect-live-update-delay 0.5)
  (setq embark-collect-live-initial-delay 0.8)
  ;; 隐藏 Embark live/completions buffers 的 modeline.
  (add-to-list 'display-buffer-alist
               '("\\`\\*Embark Collect \\(Live\\|Completions\\)\\*"
                 nil
                 (window-parameters (mode-line-format . none))))
  :bind
  (("C-;" . embark-act)
   ([remap describe-bindings] . embark-bindings)))

(use-package embark-consult
  :after (embark consult)
  :hook
  (embark-collect-mode . consult-preview-at-point-mode))
  • 使用 gnu find 命令, 需要加环境变量 ~export PATH=”/usr/local/opt/findutils/libexec/gnubin:$PATH”~

corfu

Corfu 是文本补全框架,类似于 completion-at-point 和 company-mode 包, 相比 company 的主要优势是与 Emacs 集成的 更好, 更轻量:

  1. Corfu 基于 Emacs 内置的 completion-at-point 实现, 任何使用 completion-at-point, completion-in-region 的地方 都可以使用 Corfu 来提供候选列表;
  2. 任何 completion-style 实现 (如 orderless) 都可以用来过滤候选者;
(use-package corfu
  :demand
  :straight '(corfu :host github :repo "minad/corfu")
  :init
  (defun corfu-beginning-of-prompt ()
    "Move to beginning of completion input."
    (interactive)
    (corfu--goto -1)
    (goto-char (car completion-in-region--data)))
  (defun corfu-end-of-prompt ()
    "Move to end of completion input."
    (interactive)
    (corfu--goto -1)
    (goto-char (cadr completion-in-region--data)))
  :bind
  (:map corfu-map
        ("TAB" . corfu-next)
        ([tab] . corfu-next)
        ("S-TAB" . corfu-previous)
        ([backtab] . corfu-previous)
        ;; C-a/C-e 分别移动到补全的开始和结束。
        ([remap move-beginning-of-line] . corfu-beginning-of-prompt)
        ([remap move-end-of-line] . corfu-end-of-prompt))
  :custom
  ;;开启自动补全。
  (corfu-auto t)
  (corfu-auto-prefix 2)
  (corfu-auto-delay 0.25)
  ;; 不自动选择第一个。
  (corfu-preselect-first nil)
  ;; 不自动插入候选者到光标。
  (corfu-preview-current nil)
  (corfu-min-width 80)
  (corfu-max-width corfu-min-width)
  (corfu-count 14)
  (corfu-scroll-margin 4)
  ;; 后续使用 corfu-doc 来显示文档,故关闭。
  (corfu-echo-documentation nil)
  :config
  (corfu-global-mode))

;; 总是在弹出菜单中显示候选者 
(setq completion-cycle-threshold nil)

;; 使用 TAB 来 indentation+completion(completion-at-point 默认是 M-TAB) 。
(setq tab-always-indent 'complete)

;; 在候选者右方显示文档。
(use-package corfu-doc
  :straight '(corfu-doc :host github :repo "galeo/corfu-doc")
  :after (corfu)
  :hook (corfu-mode . corfu-doc-mode)
  :bind
  (:map corfu-map
        ("M-n" . corfu-doc-sroll-up)
        ("M-p" . corfu-doc-scroll-down))
  :custom
  (corfu-doc-delay 0.3)
  (corfu-doc-max-width 70)
  (corfu-doc-max-height 20))
  • Corfu: 支持 orderless 过滤候选列表, 使用 M-SPC 来插入多个过滤模式;

yasnippet

(use-package yasnippet
  :demand
  :init
  (defvar snippet-directory "~/.emacs.d/snippets")
  (if (not (file-exists-p snippet-directory))
      (make-directory snippet-directory t))
  :commands yas-minor-mode
  :hook
  ((prog-mode org-mode  vterm-mode) . yas-minor-mode)
  :config
  (add-to-list 'yas-snippet-dirs snippet-directory)
  (yas-global-mode 1))

(use-package yasnippet-snippets :demand)

(use-package yasnippet-classic-snippets :demand)

(use-package consult-yasnippet
  :demand
  :after(consult yasnippet)
  :bind
  (:map yas-minor-mode-map
        ("C-c y" . 'consult-yasnippet)))
  • 完全输入 snippet 简写后,按 TAB 自动扩展。

goto

跳转到上次修改位置:

(use-package goto-chg
  :config
  (global-set-key (kbd "C->") 'goto-last-change)
  (global-set-key (kbd "C-<") 'goto-last-change-reverse))

跳转到特定字符或行:

(use-package avy
  :config
  ;; 值在当前 window 中跳转。
  (setq avy-all-windows nil)
  (setq avy-background t)
  :bind
  ("M-g c" . avy-goto-char-2)
  ("M-g l" . avy-goto-line))

跳转到指定窗口:

(use-package ace-window
  :init
  ;; 使用字母而非数字标记窗口,便于跳转。
  (setq aw-keys '(?a ?w ?e ?g ?i ?j ?k ?l ?p))
  ;; 根据自己的使用习惯来调整快捷键,这里使用大写字母避免与 aw-keys 冲突。
  (setq aw-dispatch-alist
        '((?0 aw-delete-window "Delete Window")
          (?1 delete-other-windows "Delete Other Windows")
          (?2 aw-split-window-vert "Split Vert Window")
          (?3 aw-split-window-horz "Split Horz Window")
          (?F aw-split-window-fair "Split Fair Window")
          (?S aw-swap-window "Swap Windows")
          (?M aw-move-window "Move Window")
          (?C aw-copy-window "Copy Window")
          ;; 为指定 window 选择新的 Buffer,并 switch 过去。
          (?B aw-switch-buffer-in-window "Select Buffer")
          ;; 为指定 window 选择新的 Buffer,切换到其它 buffer;
          (?O aw-switch-buffer-other-window "Switch Buffer Other Window")
          (?N aw-flip-window)
          (?T aw-transpose-frame "Transpose Frame")
          (?? aw-show-dispatch-help))))
:config
;; 设置为 frame 后会忽略 treemacs frame,否则即使两个窗口时也会提示选择。
(setq aw-scope 'frame)
;; 总是提示窗口选择,进而执行 ace 命令。
(setq aw-dispatch-always nil)
(global-set-key (kbd "M-o") 'ace-window)
;; 在窗口左上角显示位置字符。
;;(setq aw-char-position 'top-left)
;; 调大窗口选择字符。
(custom-set-faces
 '(aw-leading-char-face
   ((t (:inherit ace-jump-face-foreground :foreground "red" :height 1.5)))))

rime

Mac 系统安装 RIME 输入法:

  1. 下载鼠鬚管 Squirrel https://rime.im/download/,它包含输入法方案。
  2. 下载 Squirrel 使用的 librime (从 Squirrel 的 CHANGELOG 中获取版本)
  3. 重新登录用户,然后就可以使用 Control-+ 来触发 RIME 输入法了。
  4. 在 Mac 的输入法配置程序中将 鼠须管 去掉,只保留 ABC 和搜狗输入法;
  5. 部署生效,:
    • 如果修改了 ~/Library/Rime 下的配置,必须点击鼠须管的 “重新部署” 才能生效。
    • 对于 emacs-rime,如果修改了 ~/Library/Rime 下的配置,需要执行 M-x rime-deploy 生效;

下载 librime 库, emacs-rime 使用它与系统的 RIME 交互:

curl -L -O https://github.com/rime/librime/releases/download/1.7.2/rime-1.7.2-osx.zip
unzip rime-1.7.2-osx.zip -d ~/.emacs.d/librime
rm -rf rime-1.7.2-osx.zip
# 如果 MacOS Gatekeeper 阻止第三方软件运行,可以暂时关闭它:
sudo spctl --master-disable
# 后续再开启:sudo spctl --master-enable

ssnhd/rime 下载最新的词库方案安装包, 将“配置文件” 目录下的内容复制到到 ~/Library/Rime 目录。

RIME 输入法自定义缺省配置中文:

patch:
  schema_list:
    - schema: luna_pinyin_simp # 朙月拼音
    - schema: numbers # 大写数字
  menu/page_size: 9
  ascii_composer/good_old_caps_lock: true
  ascii_composer/switch_key:
    Caps_Lock: commit_code
    Shift_L: inline_ascii
    Shift_R: commit
    Control_L: commit_code
    Control_R: commit_code
  switcher/hotkeys:
  - F4
  - "Control+plus" # 使用 C-+ 调出输入法菜单
  key_binder/bindings:
  - { when: composing, accept: Shift+Tab, send: Page_Up }          # 上一页
  - { when: paging, accept: minus, send: Page_Up }                 # 上一页
  - { when: composing, accept: Tab, send: Page_Down }              # 下一页
  - { when: has_menu, accept: equal, send: Page_Down }             # 下一页
  - { when: always, accept: "Control+equal", toggle: ascii_mode}   # 中英文切换
  - { when: always, accept: "Control+period", toggle: ascii_punct} # 中英文标点切换
  - { when: always, accept: "Control+comma", toggle: full_shape}   # 全角/半角切换
# 更多快捷键参考: https://github.com/Iorest/rime-setting/blob/master/default.custom.yaml

全拼配置:

patch:
  switches:
    - name: ascii_mode # 0 中文,1 英文
      reset: 0
      states: ["中文", "西文"]
    - name: full_shape # 全角/半角符号开关
      states: ["半角", "全角"]
    - name: show_emoji # Emoji 开关
      reset: 0
      states: ["🈚️️\uFE0E", "🈶️️\uFE0F"]
    - name: zh_simp # (※1) 繁简转换
      reset: 1
      states: ["漢字", "汉字"]
    - name: symbol_support
      reset: 0 # 安装包中默认为 1, 必须设置为 0, 否则激活输入法后 emacs 卡死。
      states: [ "无符", "符" ]
  simplifier:
    option_name: zh_simp

  # 启用罕见字過濾
  engine/filters:
    - simplifier
    - simplifier@emoji_conversion
    - uniquifier
    - charset_filter@gbk # (※3) GBK 过滤
    - single_char_filter

  emoji_conversion:
    opencc_config: emoji.json
    option_name: show_emoji
    tags: abc
    #tips: all    # Emoji 显示注释

  # 改写拼写运算,含英文的词汇(luna_pinyin.cn_en.dict.yaml)不影响简拼
  "speller/algebra/@before 0": xform/^([b-df-hj-np-tv-z])$/$1_/

  # 载入朙月拼音扩充词库
  "translator/dictionary": luna_pinyin.extended

  # 加载easy_en依赖
  "schema/dependencies/@1": easy_en
  # 载入翻译英文的码表翻译器,取名为 english
  "engine/translators/@4": table_translator@english
  # english翻译器的设定项
  english:
    dictionary: easy_en
    spelling_hints: 9
    enable_completion: false # 是否启用英文输入联想补全
    enable_sentence: false # 混输时不出现带有图案的英文
    initial_quality: -0.5 # 英文候选词的位置, 数值越大越靠前。

  # 快捷表情和符号
  punctuator:
    import_preset: symbols
    symbols:
      "/fs": [½, ‰, ¼, ⅓, ⅔, ¾, ⅒]
      "/xh": [*, ×, ✱, ★, ☆, ✩, ✧, ❋, ❊, ❉, ❈, ❅, ✿, ✲]
      "/dq": [🌍, 🌎, 🌏, 🌐, 🌑, 🌒, 🌓, 🌔, 🌕, 🌖, 🌗, 🌘]
      "/sg": [🍇, 🍉, 🍌, 🍍, 🍎, 🍏, 🍑, 🍒, 🍓, 🍗, 🍦, 🎂, 🍺, 🍻]
      "/dw": [🙈, 🐵, 🐈, 🐷, 🐨, 🐼, 🐾, 🐔, 🐬, 🐠, 🦋]
      "/bq": [😀, 😁, 😂, 😃, 😄, 😅, 😆, 😉, 😊, 😋, 😎, 😍, 😘, 😗]
      "/ss": [💪, 👈, 👉, 👆, 👇, ✋, 👌, 👍, 👎, ✊, 👊, 👋, 👏, 👐]
    half_shape:
      "#": "#"
      "*": "*"
      "`": "`"
      "~": "~"
      "@": "@"
      "=": "="
      '\': ""
      "%": "%"
      "$": ["¥", "$"]
      "|": ["|", "|", "·"]
      "/": ["/", "÷"]
      "'": { pair: ["「", "」"] }
      "[": ""
      "]": ""
      "<": ""
      ">": ""

  recognizer/patterns/punct: "^/([a-z]+|[0-9]0?)$"

  # 模糊拼音
  "speller/algebra":
    - erase/^xx$/ # 第一行保留

    # 模糊音定義
    # 需要哪組就刪去行首的 # 號,單雙向任選
    - derive/^([zcs])h/$1/             # zh, ch, sh => z, c, s
    - derive/^([zcs])([^h])/$1h$2/     # z, c, s => zh, ch, sh
    #- derive/^n/l/                     # n => l
    #- derive/^l/n/                     # l => n
    - derive/([ei])n$/$1ng/            # en => eng, in => ing
    - derive/([ei])ng$/$1n/            # eng => en, ing => in
    # 以下是一組容錯拼寫,《漢語拼音》方案以前者爲正
    #- derive/^([nl])ve$/$1ue/          # nve = nue, lve = lue
    #- derive/^([jqxy])u/$1v/           # ju = jv,
    #- derive/un$/uen/                  # gun = guen,
    #- derive/ui$/uei/                  # gui = guei,
    #- derive/iu$/iou/                  # jiu = jiou,
    # 自動糾正一些常見的按鍵錯誤
    - derive/([aeiou])ng$/$1gn/        # dagn => dang
    - derive/([dtngkhrzcs])o(u|ng)$/$1o/  # zho => zhong|zhou
    - derive/ong$/on/                  # zhonguo => zhong guo
    - derive/ao$/oa/                   # hoa => hao
    - derive/([iu])a(o|ng?)$/a$1$2/    # tain => tian

  # 分尖團後 v => ü 的改寫條件也要相應地擴充:
  #'translator/preedit_format':
  #  - "xform/([nljqxyzcs])v/$1ü/"

  # librime-lua 输入动态时间和日期
  "engine/translators/@6": lua_translator@date_translator

配置 Emacs:

(use-package rime
  :ensure-system-package
  ("/Applications/SwitchKey.app" . "brew install --cask switchkey")
  :custom
  (rime-user-data-dir "~/Library/Rime/")
  (rime-librime-root "~/.emacs.d/librime/dist")
  (rime-emacs-module-header-root "/usr/local/opt/emacs-plus@28/include")
  :hook
  (emacs-startup . (lambda () (setq default-input-method "rime")))
  :bind
  ( :map rime-active-mode-map
    ;; 强制切换到英文模式,直到按回车
    ("M-j" . 'rime-inline-ascii)
    :map rime-mode-map
    ;; 中英文切换
    ("C-=" . 'rime-send-keybinding)
    ;; 输入法菜单
    ("C-+" . 'rime-send-keybinding)
    ;; 中英文标点切换
    ("C-." . 'rime-send-keybinding)
    ;; 全半角切换
    ("C-," . 'rime-send-keybinding)
    ;; 强制切换到中文模式
    ("M-j" . 'rime-force-enable))
  :config
  ;; 在 modline 高亮输入法图标, 可用来快速分辨分中英文输入状态。
  (setq mode-line-mule-info '((:eval (rime-lighter))))
  ;; support shift-l, shift-r, control-l, control-r, 只有当使用系统 RIME 输入法时才有效。
  (setq rime-inline-ascii-trigger 'shift-l)
  ;; 临时英文模式。
  (setq rime-disable-predicates
        '(rime-predicate-ace-window-p
          rime-predicate-hydra-p
          rime-predicate-current-uppercase-letter-p
          rime-predicate-after-alphabet-char-p
          rime-predicate-prog-in-code-p
          rime-predicate-after-ascii-char-p
          ))
  (setq rime-show-candidate 'posframe)

  ;; 切换到 vterm-mode 类型外的 buffer 时激活 RIME 输入法。
  (defadvice switch-to-buffer (after activate-input-method activate)
    (if (or (string-match "vterm-mode" (symbol-name major-mode))
            (string-match "minibuffer-mode" (symbol-name major-mode)))
        (activate-input-method nil)
      (activate-input-method "rime"))))
  • 使用 SwitchKey 将 Emacs 的默认系统输入法设置为英文,防止搜狗输入法干扰 RIME。

然后执行命令 M-x rime-deploy 生效。输入 weiyamu, 如果内容是 鳚亚目 则证明导入成功。

org

org

(use-package org
  :straight (org :repo "https://git.savannah.gnu.org/git/emacs/org-mode.git")
  :ensure auctex
  :demand
  :ensure-system-package
  ((watchexec . watchexec)
   (pygmentize . pygments)
   (magick . imagemagick))
  :config
  (setq org-ellipsis ".."
        org-highlight-latex-and-related '(latex)
        ;; 隐藏标记。
        org-hide-emphasis-markers t
        ;; 去掉 * 和 /, 使它们不再具有强调含义。
        org-emphasis-alist
        '(("_" underline)
          ("=" org-verbatim verbatim)
          ("~" org-code verbatim)
          ("+" (:strike-through t)))
        ;; 隐藏 block
        org-hide-block-startup t
        org-hidden-keywords '(title)
        org-cycle-separator-lines 2
        org-cycle-level-faces t
        org-n-level-faces 4
        org-tags-column -80
        org-log-into-drawer t
        org-log-done 'note
        ;; 先从 #+ATTR.* 获取宽度,如果没有设置则默认为 300 。
        org-image-actual-width '(300)
        org-export-with-broken-links t
        org-startup-folded 'content
        ;; 使用 R_{s} 形式的下标(默认是 R_s, 容易与正常内容混淆) 。
        org-use-sub-superscripts nil
        ;; export 时不处理 super/subscripting, 等效于 #+OPTIONS: ^:nil 。
        org-export-with-sub-superscripts nil
        org-startup-indented t
        ;; 支持鼠标点击链接。
        org-return-follows-link t
        org-mouse-1-follows-link t
        ;; 文件链接使用相对路径, 解决 hugo 等 image 引用的问题。
        org-link-file-path-type 'relative)
  (setq org-catch-invisible-edits 'show)
  (setq org-todo-keywords
        '((sequence "☞ TODO(t)" "PROJ(p)" "⚔ INPROCESS(s)" "⚑ WAITING(w)"
                    "|" "☟ NEXT(n)" "✰ Important(i)" "✔ DONE(d)" "✘ CANCELED(c@)")
          (sequence "✍ NOTE(N)" "FIXME(f)" "☕ BREAK(b)" "❤ Love(l)" "REVIEW(r)" )))

  ;; 支持无空格的中文强调。
  (setq org-emphasis-regexp-components
        '("-[:multibyte:][:space:]('\"{"
          "-[:multibyte:][:space:].,:!?;'\")}\\["
          "[:space:]"
          "[^=~*_]"  ;; 不允许强调字符嵌套。
          1))
  (org-set-emph-re 'org-emphasis-regexp-components org-emphasis-regexp-components)
  (org-element-update-syntax)

  (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)
  (add-hook 'org-mode-hook 'turn-on-auto-fill)
  (add-hook 'org-mode-hook (lambda () (display-line-numbers-mode 0))))

;; 自动创建和更新目录。
(use-package org-make-toc
  :config
  (add-hook 'org-mode-hook #'org-make-toc-mode))
  • pygments 实现 Latex PDF 代码语法高亮;
  • imagemagick 用于图片分辨率装换;

html

(use-package htmlize)

(setq org-html-doctype "html5")
(setq org-html-html5-fancy t)
(setq org-html-self-link-headlines t)
(setq org-html-preamble "<a name=\"top\" id=\"top\"></a>")

(use-package org-html-themify
  :straight (org-html-themify :repo "DogLooksGood/org-html-themify" :files ("*.el" "*.js" "*.css"))
  :hook
  (org-mode . org-html-themify-mode)
  :custom
  (org-html-themify-themes '((dark . doom-palenight) (light . doom-one-light))))
  • C-c C-e hhM-x org-html-export-to-html 来生成对应主题的 HTML 页面。

face

(defun my/org-faces ()
  (setq-default line-spacing 2)
  (dolist (face '((org-level-1 . 1.2)
                  (org-level-2 . 1.1)
                  (org-level-3 . 1.05)
                  (org-level-4 . 1.0)
                  (org-level-5 . 1.1)
                  (org-level-6 . 1.1)
                  (org-level-7 . 1.1)
                  (org-level-8 . 1.1)))
    (set-face-attribute (car face) nil :height (cdr face)))
  ;; 美化 BEGIN_SRC 整行。
  (setq org-fontify-whole-block-delimiter-line t)
  ;; 如果配置参数 :inherit 'fixed-pitch, 则需要明确设置 fixed-pitch 字体,
  ;; 否则选择的缺省字体可能导致显示问题。
  (custom-theme-set-faces
   'user
   '(org-block ((t (:height 0.9))))
   '(org-code ((t (:height 0.9))))
   ;; 调小高度 , 并设置下划线。
   '(org-block-begin-line ((t (:height 0.8 :underline "#A7A6AA"))))
   '(org-block-end-line ((t (:height 0.8 :underline "#A7A6AA"))))
   '(org-meta-line ((t (:height 0.7))))
   '(org-document-info-keyword ((t (:height 0.6))))
   '(org-document-info ((t (:height 0.8))))
   '(org-document-title ((t (:foreground "#ffb86c" :weight bold :height 1.5))))
   '(org-link ((t (:foreground "royal blue" :underline t))))
   '(org-property-value ((t (:height 0.8))) t)
   '(org-drawer ((t (:height 0.8))) t)
   '(org-special-keyword ((t (:height 0.8))))
   '(org-table ((t (:height 0.9))))
   '(org-verbatim ((t (:height 0.9))))
   '(org-tag ((t (:weight bold :height 0.8)))))
  (setq-default prettify-symbols-alist '(("#+BEGIN_SRC" . "»")
                                         ("#+END_SRC" . "«")
                                         ("#+begin_src" . "»")
                                         ("#+end_src" . "«")))
  (setq prettify-symbols-unprettify-at-point 'right-edge))
(add-hook 'org-mode-hook 'my/org-faces)
(add-hook 'org-mode-hook 'prettify-symbols-mode)

(use-package org-superstar
  :after (org)
  :hook
  (org-mode . org-superstar-mode)
  :custom
  (org-superstar-remove-leading-stars t)
  (org-superstar-headline-bullets-list '(""  "🞛" "" "" "")))

(use-package org-fancy-priorities
  :after (org)
  :hook
  (org-mode . org-fancy-priorities-mode)
  :config
  (setq org-fancy-priorities-list '("[A]" "[B]" "[C]")))

;; 编辑时显示隐藏的标记。
(use-package org-appear
  :config
  (add-hook 'org-mode-hook 'org-appear-mode))

fill

内容居中显示:

(defun my/org-mode-visual-fill (fill width)
  (setq-default
   ;; 自动换行的字符数。
   fill-column fill
   ;; window 可视化行宽度,值应该比 fill-column 大,否则超出的字符被隐藏。
   visual-fill-column-width width
   visual-fill-column-fringes-outside-margins nil
   ;; 使用 setq-default 来设置居中, 否则可能不生效。
   visual-fill-column-center-text t)
  (visual-fill-column-mode 1))

(use-package visual-fill-column
  :demand
  :after (org)
  :hook
  (org-mode . (lambda () (my/org-mode-visual-fill 110 130)))
  :config
  ;; 文字缩放时自动调整 visual-fill-column-width 。
  (advice-add 'text-scale-adjust :after #'visual-fill-column-adjust))
  • 如果文字居中失效, 可以执行 M-x redraw-display 命令生效。

agenda

(setq org-agenda-time-grid
      (quote ((daily today require-timed)
              (300 600 900 1200 1500 1800 2100 2400)
              "......"
              "-----------------------------------------------------"
              )))

;; org-agenda 展示的文件。
(setq org-agenda-files
      '("~/docs/orgs/gtd.org"
        "~/docs/orgs/capture.org"))
(setq org-agenda-start-day "-7d")
(setq org-agenda-span 21)
(setq org-agenda-include-diary t)
;; use org-journal
;;(setq diary-file "~/docs/orgs/diary")
;;(setq diary-mail-addr "geekard@qq.com")
;; 获取经纬度:https://www.latlong.net/
(setq calendar-latitude +39.904202)
(setq calendar-longitude +116.407394)
(setq calendar-location-name "北京")
(setq calendar-remove-frame-by-deleting t)
;; 每周第一天是周一。
(setq calendar-week-start-day 1)
;; 标记有记录的日期。
(setq mark-diary-entries-in-calendar t)
;; 标记节假日。
(setq mark-holidays-in-calendar nil)
;; 不显示节日列表。
(setq view-calendar-holidays-initially nil)
(setq org-agenda-include-diary t)

;; 除去基督徒、希伯来和伊斯兰教的节日。
(setq christian-holidays nil
      hebrew-holidays nil
      islamic-holidays nil
      solar-holidays nil
      bahai-holidays nil)

(setq mark-diary-entries-in-calendar t
      appt-issue-message nil
      mark-holidays-in-calendar t
      view-calendar-holidays-initially nil)

(setq diary-date-forms '((year "/" month "/" day "[^/0-9]"))
      calendar-date-display-form '(year "/" month "/" day)
      calendar-time-display-form '(24-hours ":" minutes (if time-zone " (") time-zone (if time-zone ")")))

(add-hook 'today-visible-calendar-hook 'calendar-mark-today)

(autoload 'chinese-year "cal-china" "Chinese year data" t)

(setq calendar-load-hook '(lambda ()
                            (set-face-foreground 'diary-face   "skyblue")
                            (set-face-background 'holiday-face "slate blue")
                            (set-face-foreground 'holiday-face "white")))

(use-package org-super-agenda)

refile

使用 outline 路径来指定要 refile 的文件和位置, 如 emacs.org/packages/org-mode:

  • packages/org-mode 是要 refile 的内容的 paret nodes, 如果不存在会提示创建。
;; refile 的位置是 agenda 文件的前三层 headline 。
(setq org-refile-targets '((org-agenda-files :maxlevel . 3)))
;; 使用文件路径的形式显示 filename 和 headline, 方便在文件的 top-head 添加内容。
(setq org-refile-use-outline-path 'file)
;; 必须设置为 nil 才能显示 headline, 否则只显示文件名 。
(setq org-outline-path-complete-in-steps nil)
;; 支持为 subtree 在 refile target 文件指定一个新的父节点 。
(setq org-refile-allow-creating-parent-nodes 'confirm)

capture

自动 Capture 浏览器发来的网址或选中的内容:

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

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

image

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

(use-package org-download
  :ensure-system-package pngpaste
  :bind
  ("<f6>" . org-download-screenshot)
  :config
  (setq-default org-download-image-dir "./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))

babel

(setq org-confirm-babel-evaluate nil)
(setq org-src-fontify-natively t)
(setq org-src-tab-acts-natively t)
;; 为 #+begin_quote 和  #+begin_verse 添加特殊 face 。
(setq org-fontify-quote-and-verse-blocks t)
;; 不自动缩进。
(setq org-src-preserve-indentation t)
(setq org-edit-src-content-indentation 0)
;; 在当前窗口编辑 SRC Block.
(setq org-src-window-setup 'current-window)

(require 'org)
(use-package ob-go)
(use-package ox-reveal)
(use-package ox-gfm)

(org-babel-do-load-languages
 'org-babel-load-languages
 '((shell . t)
   (js . t)
   (go . t)
   (emacs-lisp . t)
   (python . t)
   (dot . t)
   (css . t)))

(use-package org-contrib
  :straight (org-contrib :repo "https://git.sr.ht/~bzg/org-contrib")
  :demand)

notify

倒计时结束通知:

(use-package emacs
  :straight (:type built-in)
  :ensure-system-package terminal-notifier)

(defvar terminal-notifier-command (executable-find "terminal-notifier") "The path to terminal-notifier.")
(defun terminal-notifier-notify (title message)
  (start-process "terminal-notifier"
                 "terminal-notifier"
                 terminal-notifier-command
                 "-title" title
                 "-sound" "default"
                 "-message" message
                 "-activate" "org.gnu.Emacs"))

(defun timed-notification (time msg)
  (interactive "sNotification when (e.g: 2 minutes, 60 seconds, 3 days): \nsMessage: ")
  (run-at-time time nil (lambda (msg) (terminal-notifier-notify "Emacs" msg)) msg))

;;(terminal-notifier-notify "Emacs notification" "Something amusing happened")
(setq org-show-notification-handler (lambda (msg) (timed-notification nil msg)))

tex

在 org 文档的头部添加参数:

(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 来格式化表格。
  (setq org-latex-tables-booktabs t)
  ;; 保存 LaTeX 日志文件。
  (setq org-latex-remove-logfiles nil)
  (setq org-latex-pdf-process '("latexmk -xelatex -quiet -shell-escape -f %f"))
  ;; ;; Alist of packages to be inserted in every LaTeX header.
  ;; (setq org-latex-packages-alist
  ;;       (quote (("" "color" t)
  ;;               ("" "xcolor" t)
  ;;               ("" "listings" t)
  ;;               ("" "fontspec" t)
  ;;               ("" "parskip" t) ;; 增加正文段落的间距
  ;;               ("AUTO" "inputenc" t))))
  (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}")))
  ;; 自定义 latex 语言环境(基于 tcolorbox), 参考:https://blog.shimanoke.com/ja/posts/output-latex-code-with-tcolorbox/
  (setq org-latex-custom-lang-environments
        '((c "\\begin{programlist}[label={%l}]{c}{: %c}\n%s\\end{programlist}")
          (ditaa "\\begin{programlist}[label={%l}]{text}{: %c}\n%s\\end{programlist}")
          (emacs-lisp "\\begin{programlist}[label={%l}]{lisp}{: %c}\n%s\\end{programlist}")
          (ruby "\\begin{programlist}[label={%l}]{ruby}{: %c}\n%s\\end{programlist}")
          (latex "\\begin{programlist}[label={%l}]{latex}{: %c}\n%s\\end{programlist}")
          (go "\\begin{programlist}[label={%l}]{go}{: %c}\n%s\\end{programlist}")
          (lua "\\begin{programlist}[label={%l}]{lua}{: %c}\n%s\\end{programlist}")
          (java "\\begin{programlist}[label={%l}]{java}{: %c}\n%s\\end{programlist}")
          (javascript "\\begin{programlist}[label={%l}]{javascript}{: %c}\n%s\\end{programlist}")
          (json "\\begin{programlist}[label={%l}]{json}{: %c}\n%s\\end{programlist}")
          (plantuml "\\begin{programlist}[label={%l}]{text}{: %c}\n%s\\end{programlist}")
          (yaml "\\begin{programlist}[label={%l}]{yaml}{: %c}\n%s\\end{programlist}")
          (maxima "\\begin{programlist}[label={%l}]{text}{: %c}\n%s\\end{programlist}")
          (ipython "\\begin{programlist}[label={%l}]{python}{: %c}\n%s\\end{programlist}")
          (python "\\begin{programlist}[label={%l}]{python}{: %c}\n%s\\end{programlist}")
          (perl "\\begin{programlist}[label={%l}]{perl}{: %c}\n%s\\end{programlist}")
          (html "\\begin{programlist}[label={%l}]{html}{: %c}\n%s\\end{programlist}")
          (org "\\begin{programlist}[label={%l}]{text}{: %c}\n%s\\end{programlist}")
          (typescript "\\begin{programlist}[label={%l}]{typescript}{: %c}\n%s\\end{programlist}")
          (scss "\\begin{programlist}[label={%l}]{scss}{: %c}\n%s\\end{programlist}")
          (sh "\\begin{programlist}[label={%l}]{shell}{: %c}\n%s\\end{programlist}")
          (bash "\\begin{programlist}[label={%l}]{shell}{: %c}\n%s\\end{programlist}")
          (shell "\\begin{programlist}[label={%l}]{shell}{: %c}\n%s\\end{programlist}")
          (shellinput "\\begin{shellinput}[%c]\n%s\\end{shellinput}")
          (shelloutput "\\begin{shelloutput}[%c]\n%s\\end{shelloutput}")))
  (setq org-latex-listings 'listings))
  • minted 包提供代码语法高亮的功能(TexLive 默认安装), 它依赖 pygements 。
  • 变量 org-latex-minted-langs 列出 Emacs Major-Mode 与 minted 语言类型(pygmentize -L lexers)的关系, 如果两者 一致(如 go-[mod] 和 go), 则不需要列出。
  • minted 的 fontfamily 只对预定义的 tt/courier/helvetica 有效。

自定义样式 mystyle.sty:

\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}

% 安装荧光笔效果的强调宏包 breakfbox(https://blog.shimanoke.com/ja/posts/change-latex-emph/)
% 1. 克隆 https://github.com/doraTeX/breakfbox 到 /usr/local/texlive/texmf-local/tex/latex
% 2. 刷新数据库:  sudo mktexlsr

% 黄色背景高亮强调(来源于 breakfbox)
\usepackage{uline--}
\renewcommand{\emph}[1]{
  {\sffamily\bfseries\itshape
    \uline[
      background,
      color={[rgb]{1,1,0.0}},
      width=0.8em,position=1pt]{#1}}}

% 自定义 programlist 语言环境
% https://blog.shimanoke.com/ja/posts/output-latex-code-with-tcolorbox/
\usepackage{tcolorbox}
\tcbuselibrary{breakable,skins,raster,external,listings,minted}
\tcbEXTERNALIZE
\newtcblisting[
  auto counter,
  number within=section]{programlist}[3][]{
  listing engine=minted,
  minted style=emacs,
  minted language=#2,
  minted options={autogobble,fontsize=\footnotesize,breaklines,breakanywhere,baselinestretch=1.2,linenos,numbersep=3mm},
  % 不显示 title
  %title={\sffamily\bfseries 代码块 \thetcbcounter},
  %title={\sffamily\bfseries 代码块 \thetcbcounter #3},
  after,
  breakable=true,
  lowerbox=ignored,
  hyphenationfix=true,
  %边框颜色
  %colback=blue!5!white,
  %colframe=blue!85!black,
  listing only,
  enhanced,
  drop fuzzy shadow southeast,
  left=5mm,
  overlay={\begin{tcbclipinterior}\fill[red!20!blue!20!white] (frame.south west) rectangle ([xshift=5mm]frame.north west);\end{tcbclipinterior}},
  #1
}

% 提示 title
\usepackage[explicit]{titlesec}
\usepackage{titling}
\setlength{\droptitle}{-6em}

% 超链接
\usepackage[colorlinks]{hyperref}
\hypersetup{
  pdfborder={0 0 0},
  colorlinks=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}}}

% tabularx 的特殊 align 参数 X 用来对指定列内容自动换行,表格前需要加如下属性:
% #+ATTR_LATEX: :environment tabularx :booktabs t :width \linewidth :align l|X
\usepackage{tabularx}
% 美化表格显示效果
\usepackage{booktabs}
% 表格隔行颜色, {1} 开始行, {lightgrep} 奇数行颜色, {} 偶数行颜色(空表示白色)
\rowcolors{1}{lightgrey}{}

\usepackage{parskip}
\setlength{\parskip}{1em}
\setlength{\parindent}{0pt}

\usepackage{etoolbox}
\usepackage{calc}

\usepackage[scale=0.85]{geometry}
%\setlength{\headsep}{5pt}

\usepackage{amsthm}
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{indentfirst}
\usepackage{multicol}
\usepackage{multirow}
\usepackage{linegoal}
\usepackage{graphicx}
\usepackage{fancyvrb}
\usepackage{abstract}
\usepackage{hologo}

\linespread{1.25}
\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

(use-package org-tree-slide
  :after (org)
  :commands org-tree-slide-mode
  :bind
  (:map org-mode-map
        ("<f8>" . org-tree-slide-mode)
        :map org-tree-slide-mode-map
        ("<f9>" . org-tree-slide-content)
        ("<left>" . org-tree-slide-move-previous-tree)
        ("<right>" . org-tree-slide-move-next-tree))
  :hook
  ((org-tree-slide-play . (lambda ()
                            (blink-cursor-mode +1)
                            (setq-default x-stretch-cursor -1)
                            (beacon-mode -1)
                            (redraw-display)
                            (org-display-inline-images)
                            (text-scale-increase 1)
                            (centaur-tabs-mode 0)
                            (read-only-mode 1)))
   (org-tree-slide-stop . (lambda ()
                            (blink-cursor-mode +1)
                            (setq-default x-stretch-cursor t)
                            (text-scale-increase 0)
                            (beacon-mode +1)
                            (centaur-tabs-mode 1)
                            (read-only-mode -1))))
  :config
  (setq org-tree-slide-header nil)
  (setq org-tree-slide-heading-emphasis nil)
  (setq org-tree-slide-slide-in-effect t)
  (setq org-tree-slide-content-margin-top 0)
  (setq org-tree-slide-activate-message " ")
  (setq org-tree-slide-deactivate-message " ")
  (setq org-tree-slide-modeline-display nil)
  (setq org-tree-slide-breadcrumbs " 👉 ")
  ;; 隐藏 #+KEYWORD 行内容。
  (defun +org-present-hide-blocks-h ()
    (save-excursion
      (goto-char (point-min))
      (while (re-search-forward "^[[:space:]]*\\(#\\+\\)\\(\\(?:BEGIN\\|END\\|begin\\|end\\|ATTR\\|DOWNLOADED\\)[^[:space:]]+\\).*" nil t)
        (org-flag-region (match-beginning 0) (match-end 0) org-tree-slide-mode t))))
  (add-hook 'org-tree-slide-play-hook #'+org-present-hide-blocks-h))
  • 如果文字居中失效, 可以执行 M-x redraw-display 命令来生效。

journal

;; 设置缺省 prefix key, 必须在加载 org-journal 前设置。
(setq org-journal-prefix-key "C-c j")

(use-package org-journal
  :demand
  :commands org-journal-new-entry
  :init
  (defun org-journal-save-entry-and-exit()
    (interactive)
    (save-buffer)
    (kill-buffer-and-window))
  :bind
  (:map org-journal-mode-map
        ("C-c C-j" . 'org-journal-new-entry)
        ("C-c C-e" . 'org-journal-save-entry-and-exit))
  :config
  (setq org-journal-file-type 'monthly)
  (setq org-journal-dir "~/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 。

elfeed

(use-package elfeed
  :demand
  :config
  (setq elfeed-db-directory (expand-file-name "elfeed" user-emacs-directory))
  (setq elfeed-show-entry-switch 'display-buffer)
  (setq elfeed-curl-timeout 30)
  (setf url-queue-timeout 40)
  (push "-k" elfeed-curl-extra-arguments)
  (setq elfeed-search-filter "@1-months-ago +unread")
  ;; 在同一个 buffer 中显示条目。
  (setq elfeed-show-unique-buffers nil)
  (setq elfeed-search-title-max-width 150)
  (setq elfeed-search-date-format '("%Y-%m-%d %H:%M" 20 :left))
  (setq elfeed-log-level 'warn)

  ;; 支持收藏 feed, 参考:http://pragmaticemacs.com/emacs/star-and-unstar-articles-in-elfeed/
  (defalias 'elfeed-toggle-star (elfeed-expose #'elfeed-search-toggle-all 'star))
  (eval-after-load 'elfeed-search '(define-key elfeed-search-mode-map (kbd "m") 'elfeed-toggle-star))
  (defface elfeed-search-star-title-face '((t :foreground "#f77")) "Marks a starred Elfeed entry.")
  (push '(star elfeed-search-star-title-face) elfeed-search-face-alist))

(use-package elfeed-org
  :custom ((rmh-elfeed-org-files (list "~/.emacs.d/elfeed.org")))
  :hook
  ((elfeed-dashboard-mode . elfeed-org)
   (elfeed-show-mode . elfeed-org))
  :config
  (progn
    (defun my/reload-org-feeds ()
      (interactive)
      (rmh-elfeed-org-process rmh-elfeed-org-files rmh-elfeed-org-tree-id))
    (advice-add 'elfeed-dashboard-update :before #'my/reload-org-feeds)))

(use-package elfeed-dashboard
  :config
  (global-set-key (kbd "C-c f") 'elfeed-dashboard)
  (setq elfeed-dashboard-file "~/.emacs.d/elfeed-dashboard.org")
  (advice-add 'elfeed-search-quit-window :after #'elfeed-dashboard-update-links))

(use-package elfeed-score
  :config
  (progn
    (elfeed-score-enable)
    (define-key elfeed-search-mode-map "=" elfeed-score-map)))

(use-package elfeed-goodies
  :config
  (setq elfeed-goodies/entry-pane-position 'bottom)
  (setq elfeed-goodies/feed-source-column-width 30)
  (setq elfeed-goodies/tag-column-width 30)
  (setq elfeed-goodies/powerline-default-separator 'arrow)
  (elfeed-goodies/setup))

;; elfeed-goodies 显示日期栏
;;https://github.com/algernon/elfeed-goodies/issues/15#issuecomment-243358901
(defun elfeed-goodies/search-header-draw ()
  "Returns the string to be used as the Elfeed header."
  (if (zerop (elfeed-db-last-update))
      (elfeed-search--intro-header)
    (let* ((separator-left (intern (format "powerline-%s-%s"
                                           elfeed-goodies/powerline-default-separator
                                           (car powerline-default-separator-dir))))
           (separator-right (intern (format "powerline-%s-%s"
                                            elfeed-goodies/powerline-default-separator
                                            (cdr powerline-default-separator-dir))))
           (db-time (seconds-to-time (elfeed-db-last-update)))
           (stats (-elfeed/feed-stats))
           (search-filter (cond
                           (elfeed-search-filter-active
                            "")
                           (elfeed-search-filter
                            elfeed-search-filter)
                           (""))))
      (if (>= (window-width) (* (frame-width) elfeed-goodies/wide-threshold))
          (search-header/draw-wide separator-left separator-right search-filter stats db-time)
        (search-header/draw-tight separator-left separator-right search-filter stats db-time)))))

(defun elfeed-goodies/entry-line-draw (entry)
  "Print ENTRY to the buffer."
  (let* ((title (or (elfeed-meta entry :title) (elfeed-entry-title entry) ""))
         (date (elfeed-search-format-date (elfeed-entry-date entry)))
         (title-faces (elfeed-search--faces (elfeed-entry-tags entry)))
         (feed (elfeed-entry-feed entry))
         (feed-title
          (when feed
            (or (elfeed-meta feed :title) (elfeed-feed-title feed))))
         (tags (mapcar #'symbol-name (elfeed-entry-tags entry)))
         (tags-str (concat "[" (mapconcat 'identity tags ",") "]"))
         (title-width (- (window-width) elfeed-goodies/feed-source-column-width
                         elfeed-goodies/tag-column-width 4))
         (title-column (elfeed-format-column
                        title (elfeed-clamp
                               elfeed-search-title-min-width
                               title-width
                               title-width)
                        :left))
         (tag-column (elfeed-format-column
                      tags-str (elfeed-clamp (length tags-str)
                                             elfeed-goodies/tag-column-width
                                             elfeed-goodies/tag-column-width)
                      :left))
         (feed-column (elfeed-format-column
                       feed-title (elfeed-clamp elfeed-goodies/feed-source-column-width
                                                elfeed-goodies/feed-source-column-width
                                                elfeed-goodies/feed-source-column-width)
                       :left)))

    (if (>= (window-width) (* (frame-width) elfeed-goodies/wide-threshold))
        (progn
          (insert (propertize date 'face 'elfeed-search-date-face) " ")
          (insert (propertize feed-column 'face 'elfeed-search-feed-face) " ")
          (insert (propertize tag-column 'face 'elfeed-search-tag-face) " ")
          (insert (propertize title 'face title-faces 'kbd-help title)))
      (insert (propertize title 'face title-faces 'kbd-help title)))))

elfeed-score 规则文件(语法参考):

;;; Elfeed score file                                     -*- lisp -*-
(
;; ("title"
;;   (:text "opsnull" :value 250 :type S))
;;  ("content"
;;   (:text "type erasure" :value 500 :type s))
 ("title-or-content"
;;  (:text "emacs" :title-value 150 :content-value 100 :type s)
  (:text "opsnull" :title-value 150 :content-value 100 :type w))
 ("feed"
  (:text "Irreal" :value 250 :type S :attr t)
  (:text "emacs-news – sacha chua" :value 350 :type S :attr t :comment "Essential!"))
;; ("authors"
;;  (:text "opsnull" :value 500 :type s))
;; ("tag"
;;  (:tags (t . reddit-question)
;;         :value 750
;;         :comment "Add 750 points to any entry with a tag of reddit-question"))
 (mark -2500))

magit

magit 是全宇宙最强大、最好用的 git 客户端,没有之一!

(setq vc-follow-symlinks t)
(use-package magit
  :straight (magit :repo "magit/magit" :files ("lisp/*.el"))
  :custom
  ;; 在当前 window 中显示 magit buffer.
  (magit-display-buffer-function #'magit-display-buffer-same-window-except-diff-v1)
  (magit-commit-ask-to-stage nil)
  ;; 默认不选中 magit buffer.
  (magit-display-buffer-noselect t)
  (magit-log-arguments '("--graph" "--decorate" "--color"))
  :config
  ;; kill 所有 magit buffer.
  (defun my-magit-kill-buffers (&rest _)
    "Restore window configuration and kill all Magit buffers."
    (interactive)
    (magit-restore-window-configuration)
    (let ((buffers (magit-mode-get-buffers)))
      (when (eq major-mode 'magit-status-mode)
        (mapc (lambda (buf)
                (with-current-buffer buf
                  (if (and magit-this-process
                           (eq (process-status magit-this-process) 'run))
                      (bury-buffer buf)
                    (kill-buffer buf))))
              buffers))))
  (setq magit-bury-buffer-function #'my-magit-kill-buffers))
  • (setq auto-revert-check-vc-info t) 自动 revert buffer,确保 modeline 上的分支名正确,但是 CPU Profile 显示比 较影响性能,故暂不开启。

git-link 根据仓库地址、commit 等信息为光标位置生成 URL:

(use-package git-link
  :config
  (global-set-key (kbd "C-c g l") 'git-link)
  (setq git-link-use-commit t))

diff

;; diff 时显示空白字符。
(defun my/diff-spaces ()
  (setq-local whitespace-style
              '(face
                tabs
                tab-mark
                spaces
                space-mark
                trailing
                indentation::space
                indentation::tab
                newline
                newline-mark))
  (whitespace-mode 1))

(use-package diff-mode
  :straight (:type built-in)
  :init
  (setq diff-default-read-only t)
  (setq diff-advance-after-apply-hunk t)
  (setq diff-update-on-the-fly t)
  (setq diff-refine nil)
  ;; better for patches
  (setq diff-font-lock-prettify nil)
  :config
  (add-hook 'diff-mode-hook 'my/diff-spaces))

(use-package ediff
  :straight (:type built-in)
  :config
  (setq ediff-keep-variants nil)
  ;; 忽略空格
  (setq ediff-diff-options "-w")
  (setq ediff-split-window-function 'split-window-horizontally)
  ;; 不创建新的 frame 来显示 Control-Panel
  (setq ediff-window-setup-function #'ediff-setup-windows-plain)
  (add-hook 'ediff-mode-hook 'my/diff-spaces)
  ;; 启动 ediff 前关闭 treemacs frame, 否则 Control-Panel 显示异常
  (add-hook 'ediff-before-setup-hook
            (lambda ()
              (require 'treemacs)
              (if (string-match "visible" (symbol-name (treemacs-current-visibility)))
                  (delete-window (treemacs-get-local-window)) ) ))

  ;; ediff 时自动展开 org-mode, https://dotemacs.readthedocs.io/en/latest/#ediff
  (defun f-ediff-org-showhide (buf command &rest cmdargs)
    "If buffer exists and is orgmode then execute command"
    (when buf
      (when (eq (buffer-local-value 'major-mode (get-buffer buf)) 'org-mode)
        (save-excursion (set-buffer buf) (apply command cmdargs)))))

  (defun f-ediff-org-unfold-tree-element ()
    "Unfold tree at diff location"
    (f-ediff-org-showhide ediff-buffer-A 'org-reveal)
    (f-ediff-org-showhide ediff-buffer-B 'org-reveal)
    (f-ediff-org-showhide ediff-buffer-C 'org-reveal))

  (defun f-ediff-org-fold-tree ()
    "Fold tree back to top level"
    (f-ediff-org-showhide ediff-buffer-A 'hide-sublevels 1)
    (f-ediff-org-showhide ediff-buffer-B 'hide-sublevels 1)
    (f-ediff-org-showhide ediff-buffer-C 'hide-sublevels 1))

  ;; disable ligatures in ediff completely
  (add-hook 'ediff-mode-hook (lambda () (setq auto-composition-mode nil)))
  (add-hook 'ediff-select-hook 'f-ediff-org-unfold-tree-element)
  (add-hook 'ediff-unselect-hook 'f-ediff-org-fold-tree))

programming

flycheck

flycheck 是现代的在线语法检查工具, 用于取代 emacs 内置的 flymake 工具。它使用系统安装的工具对 buffer 进行检查:

C-c ! v (flycheck-verify-setup)
查看当前 buffer 使用 checker(默认使用 lsp checker) 。
C-c ! l (flycheck-list-errors)
列出当前 workspace 所有 error 。
  • M-g f 或 C-c !! (consult-flycheck) 。
(use-package flycheck
  :demand
  :config
  ;; 高亮出现错误的列位置。
  (setq flycheck-highlighting-mode (quote columns))
  ;; save 而非 idle-change 时检查, 避免不必要的错误提示。
  (setq flycheck-check-syntax-automatically '(save mode-enabled))
  (define-key flycheck-mode-map (kbd "M-g n") #'flycheck-next-error)
  (define-key flycheck-mode-map (kbd "M-g p") #'flycheck-previous-error)
  :hook
  (prog-mode . flycheck-mode))

;; 在线显示 flycheck 错误。
(use-package flycheck-pos-tip
  :after (flycheck)
  :config
  (flycheck-pos-tip-mode))

;; flycheck 实时预览。
(use-package consult-flycheck
  :after (consult flycheck)
  :bind
  (:map flycheck-command-map ("!" . consult-flycheck)))

lsp

(setenv "LSP_USE_PLISTS" "true")

(use-package lsp-mode
  :custom
  ;; debug 时才开启 log, 否则影响性能。
  (lsp-log-io nil)
  ;; 日志记录行数。
  (lsp-log-max 1000)
  (lsp-keymap-prefix "C-c l")
  (lsp-diagnostics-provider :flycheck)
  (lsp-diagnostics-flycheck-default-level 'warning)
  (lsp-completion-provider :none) ;; corfu.el: :none, company: :capf
  (lsp-enable-symbol-highlighting nil)
  ;; 不显示面包屑。
  (lsp-headerline-breadcrumb-enable nil)
  (lsp-headerline-breadcrumb-segments '(path-up-to-project file symbols))
  ;; 启用 snippet 后才支持函数或方法的 placeholder 提示。
  (lsp-enable-snippet nil)
  ;; 后续使用 lsp-ui-doc 替代 eldoc, 前者还支持 mouse 和 cursor hover.
  (lsp-eldoc-enable-hover nil)
  (lsp-eldoc-render-all t)
  ;; 刷新高亮、lenses 和 links 的间隔。
  (lsp-idle-delay 0.2)
  ;; 退出最后一个 lsp buffer 时自动 kill lsp-server.
  (lsp-keep-workspace-alive nil)
  (lsp-enable-file-watchers nil)
  ;; 关闭 folding 。
  (lsp-enable-folding nil)
  ;; lsp 显示的 links 不准确且导致 treemacs 目录显示异常,故关闭。
  (lsp-enable-links nil)
  (lsp-enable-indentation nil)
  ;; flycheck 会在 modeline 展示检查结果, 故不需 lsp 再展示。
  (lsp-modeline-diagnostics-enable nil)
  ;; 不在 modeline 上显示 code-actions 信息。
  (lsp-modeline-code-actions-enable nil)
  (lsp-modeline-workspace-status-enable nil)
  (lsp-restart 'auto-restart)
  ;; 使用 projectile/project 来自动探测项目根目录。
  (lsp-auto-guess-root t)
  ;; 不对 imenu 结果进行排序.
  (lsp-imenu-sort-methods '(position))
  :init
  ;; 设置 lsp 使用 corfu 来进行补全。
  (defun my/lsp-mode-setup-completion ()
    (setf (alist-get 'styles (alist-get 'lsp-capf completion-category-defaults))
          '(orderless)))
  :hook
  ((java-mode . lsp)
   (python-mode . lsp)
   (go-mode . lsp)
   ;;(yaml-mode . lsp)
   ;;(js-mode . lsp)
   (web-mode . lsp)
   (tide-mode . lsp)
   (typescript-mode . lsp)
   (dockerfile-mode . lsp)
   (lsp-completion-mode . my/lsp-mode-setup-completion))
  :config
  (dolist (dir '("[/\\\\][^/\\\\]*\\.\\(json\\|html\\|pyc\\|class\\|log\\|jade\\|md\\)\\'"
                 "[/\\\\]resources/META-INF\\'"
                 "[/\\\\]vendor\\'"
                 "[/\\\\]node_modules\\'"
                 "[/\\\\]\\.settings\\'"
                 "[/\\\\]\\.project\\'"
                 "[/\\\\]\\.travis\\'"
                 "[/\\\\]bazel-*"
                 "[/\\\\]\\.cache"
                 "[/\\\\]\\.clwb$"))
    (push dir lsp-file-watch-ignored-directories))
  :bind
  (:map lsp-mode-map
        ("C-c f" . lsp-format-region)
        ("C-c e" . lsp-describe-thing-at-point)
        ("C-c a" . lsp-execute-code-action)
        ("C-c r" . lsp-rename)
        ([remap xref-find-definitions] . lsp-find-definition)
        ([remap xref-find-references] . lsp-find-references)))

consult-lsp 提供两个非常有用的命令:consult-lsp-symbols 和 consult-lsp-diagnostics:

  • consult-lsp-symbols: C-M-.
(use-package consult-lsp
  :after (lsp-mode consult)
  :config
  (define-key lsp-mode-map [remap xref-find-apropos] #'consult-lsp-symbols))

lsp-ui 显示函数签名和帮助文档:

  • lsp-mode 和 lsp-ui 的特性可以参考这个页面来进行选择性的打开和关闭;
(use-package lsp-ui
  :after (lsp-mode flycheck)
  :custom
  ;; 显示目录。
  (lsp-ui-peek-show-directory t)
  ;; 文件列表宽度。
  (lsp-ui-peek-list-width 80)
  (lsp-ui-doc-delay 0.1)
  ;;(lsp-ui-doc-position 'at-point)
  ;; 启用 flycheck 集成。
  (lsp-ui-flycheck-enable t)
  (lsp-ui-sideline-enable nil)
  (lsp-ui-peek-fontify 'always)
  :config
  (define-key lsp-ui-mode-map [remap xref-find-definitions] #'lsp-ui-peek-find-definitions)
  (define-key lsp-ui-mode-map [remap xref-find-references] #'lsp-ui-peek-find-references))

python

pyenv

pyenvpyenv-virtualen 可以为项目或系统指定不同隔离的 python 或 venv 版本。

(use-package emacs
  :straight (:type built-in)
  :ensure-system-package
  ((pyenv . "brew install --HEAD pyenv")
   (pyenv-virtualenv . "brew install --HEAD pyenv-virtualenv")))

为了在进入项目目录时自动切换到指定 pyenv 或 venv 版本,需要配置 ~/.bashrc

eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"
eval "$(jenv init -)"

pyenv 工作流:

  1. 列出可以安装的 python 版本: pyenv install -l
  2. 安装指定的 python 版本: pyenv install <version>
  3. 创建一个 pyenv virtualenv: pyenv virtualenv [version] <virtualenv-name>
  4. 为项目指定 python 版本或上一步创建的 virtualenv 名称:
    • 在项目根目录执行 pyenv local <version1> <version2>
  5. 指定用户默认使用 3.9.0 python 版本:
    • 在家目录执行命令: cd ~ && pyenv local 3.9.0
    • 后续家目录下执行 pip 命令安装的包都是 3.x 版本。
  6. 如果虚拟环境中没有 pip 命令,安装: python -m ensurepip

python-mode

(defun my/python-setup-shell (&rest args)
  (if (executable-find "ipython")
      (progn
        (setq python-shell-interpreter "ipython")
        (setq python-shell-interpreter-args "--simple-prompt -i"))
    (progn
      (setq python-shell-interpreter "python")
      (setq python-shell-interpreter-args "-i"))))

(defun my/python-setup-checkers (&rest args)
  (when (fboundp 'flycheck-set-checker-executable)
    (let ((pylint (executable-find "pylint"))
          (flake8 (executable-find "flake8")))
      (when pylint
        (flycheck-set-checker-executable "python-pylint" pylint))
      (when flake8
        (flycheck-set-checker-executable "python-flake8" flake8)))))

(use-package python
  :ensure-system-package
  ((pylint . pylint)
   (flake8 . flake8)
   (ipython . "pip install ipython"))
  :init
  (setq python-indent-guess-indent-offset t)  
  (setq python-indent-guess-indent-offset-verbose nil)
  (setq python-indent-offset 4)
  (with-eval-after-load 'exec-path-from-shell (exec-path-from-shell-copy-env "PYTHONPATH"))
  :hook
  (python-mode . (lambda ()
                   (my/python-setup-shell)
                   (my/python-setup-checkers))))
  • 需要在对应的 python env 中安装 pylint/flake8/yapf 程序。

lsp-pyright

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

  ;; 使用 yapf 格式化 python 代码。
(use-package yapfify
  :straight (:host github :repo "JorisE/yapfify"))

(use-package lsp-pyright
  :after (python)
  :ensure-system-package
  ((pyright . "sudo npm update -g pyright")
   (yapf . "pip install yapf"))
  :init
  (defvar pyright-directory "~/.emacs.d/.cache/lsp/npm/pyright/lib")
  (if (not (file-exists-p pyright-directory))
      (make-directory pyright-directory t))
  (when (executable-find "python3")
    (setq lsp-pyright-python-executable-cmd "python3"))
  :hook
  (python-mode . (lambda () (require 'lsp-pyright) (yapf-mode))))

pyright 不使用 pyenv .python-version 指定的 python 版本或 venv 来搜索依赖的 module,而是使用 pyrightconfig.json 文件中配置的 venv 和 venvPath:

  • venvPath:指定查找 venv 目录的上级目录,可以包含多个 venv 环境;
  • venv:指定 venvPath 目录下的、使用的虚拟环境名称, pyright 在该 venv 中搜索依赖的 package;

安装 pyenv-pyright 插件来方便的创建和更新 pyrightconfig.json 文件:

git clone https://github.com/alefpereira/pyenv-pyright.git $(pyenv root)/plugins/pyenv-pyright

使用方法:

  1. 使用 pyenv local 为项目指定 pyenv virtualenv;
  2. 使用 pyenv pyright 来自动配置 pyrightconfig.json 使用上一步指定的 virtualenv;

pyright 假设源文件位于项目 scr 目录下,但实际可能会在多个其它子目录(甚至嵌套情况)中放置项目源码,即 multi-root 模式(对应于 vscode 中的多 worksapce 目录),这时可能出现大量 import 错误,可以通过在项目根目录配置 pyrightconfig.json 文件来解决,例如(参考:python module Import Resolution):

{
    "venv": "venv-2.7.18",
    "venvPath": "/Users/zhangjun/.pyenv/versions",
    "verboseOutput": true,
    "reportMissingTypeStubs": false,
    "executionEnvironments": [
        {
            "root": "scripts",
            "extraPaths": [
                ".",  // scripts 目录下 py 文件导入同级 py 文件的情况
                "scripts/appinstance_apply"
            ]
        }
    ]
}

executionEnvironments:

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

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

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

go

(use-package go-mode
  :after (lsp-mode)
  :ensure-system-package (gopls . "go install golang.org/x/tools/gopls@latest")
  :init
  (setq godoc-at-point-function #'godoc-gogetdoc)
  (defun lsp-go-install-save-hooks ()
    (add-hook 'before-save-hook #'lsp-format-buffer t t)
    (add-hook 'before-save-hook #'lsp-organize-imports t t))
  :hook (go-mode . lsp-go-install-save-hooks)
  :bind
  (:map go-mode-map
        ("C-c R" . go-remove-unused-imports)
        ("<f1>" . godoc-at-point))
  :config
  ;; 配置 -mod=mod, 防止带有 vendor 目录的项目报错: go: inconsistent vendoring
  (setq lsp-go-env '((GOFLAGS . "-mod=mod")))
  (lsp-register-custom-settings
   `(("gopls.allExperiments" t t)
     ("gopls.staticcheck" t t)
     ("gopls.completeUnimported" t t)
     ;; opts a user into the experimental support for multi-module workspaces
     ("gopls.experimentalWorkspaceModule" t t)
     ;;disables -mod=readonly, allowing imports from out-of-scope module
     ("gopls.allowModfileModifications" t t)
     ;;disables GOPROXY=off, allowing implicit module downloads rather than requiring user action
     ("gopls.allowImplicitNetworkAccess" t t)
     ;; enables gopls to fall back on outdated package metadata
     ("gopls.experimentalUseInvalidMetadata" t t))))

(use-package flycheck-golangci-lint
  :ensure-system-package
  (golangci-lint)
  :after flycheck
  :defines flycheck-disabled-checkers
  :hook (go-mode . (lambda ()
                     "Enable golangci-lint."
                     (setq flycheck-disabled-checkers '(go-gofmt
                                                        go-golint
                                                        go-vet
                                                        go-build
                                                        go-test
                                                        go-staticcheck
                                                        go-errcheck))
                     (flycheck-golangci-lint-setup))))
  • 需要开启 gopls.experimentalWorkspaceModule 支持嵌入式 module, 否则可能出错:

emacs errors loading workspace: You are working in a nested module. Please open it as a separate workspace folder. Learn more:

;; 安装或更新工具。
(defvar go--tools '("golang.org/x/tools/gopls"
                    "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)

(use-package go-tag
  :bind (:map go-mode-map
              ("C-c t a" . go-tag-add)
              ("C-c t r" . go-tag-remove))
  :init (setq go-tag-args (list "-transform" "camelcase")))

(use-package go-gen-test
  :bind (:map go-mode-map
              ("C-c t g" . go-gen-test-dwim)))

(use-package gotest
  :bind (:map go-mode-map
              ("C-c t f" . go-test-current-file)
              ("C-c t t" . go-test-current-test)
              ("C-c t j" . go-test-current-project)
              ("C-c t b" . go-test-current-benchmark)
              ("C-c t c" . go-test-current-coverage)
              ("C-c t x" . go-run)))

(use-package go-playground
  :diminish
  :commands (go-playground-mode))

markdown

multimarkdown 将 markdown 转换为 html 进行 preview,可以结合 xwidget webkit 或 grip 进行实时预览:

(use-package markdown-mode
  :ensure-system-package multimarkdown
  :commands (markdown-mode gfm-mode)
  :mode
  (("README\\.md\\'" . gfm-mode)
   ("\\.md\\'" . markdown-mode)
   ("\\.markdown\\'" . markdown-mode))
  :init
  (when (executable-find "multimarkdown")
    (setq markdown-command "multimarkdown"))
  (setq markdown-enable-wiki-links t)
  (setq markdown-italic-underscore t)
  (setq markdown-asymmetric-header t)
  (setq markdown-make-gfm-checkboxes-buttons t)
  (setq markdown-gfm-uppercase-checkbox t)
  (setq markdown-fontify-code-blocks-natively t)
  (setq markdown-gfm-additional-languages "Mermaid")
  (setq markdown-content-type "application/xhtml+xml")
  (setq markdown-css-paths '("https://cdn.jsdelivr.net/npm/github-markdown-css/github-markdown.min.css"
                             "https://cdn.jsdelivr.net/gh/highlightjs/cdn-release/build/styles/github.min.css"))
  (setq markdown-xhtml-header-content "
<meta name='viewport' content='width=device-width, initial-scale=1, shrink-to-fit=no'>
<style>
body {
  box-sizing: border-box;
  max-width: 740px;
  width: 100%;
  margin: 40px auto;
  padding: 0 10px;
}
</style>
<link rel='stylesheet' href='https://cdn.jsdelivr.net/gh/highlightjs/cdn-release/build/styles/default.min.css'>
<script src='https://cdn.jsdelivr.net/gh/highlightjs/cdn-release/build/highlight.min.js'></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
  document.body.classList.add('markdown-body');
  document.querySelectorAll('pre code').forEach((code) => {
    if (code.className != 'mermaid') {
      hljs.highlightBlock(code);
    }
  });
});
</script>
<script src='https://unpkg.com/mermaid@8.4.8/dist/mermaid.min.js'></script>
<script>
mermaid.initialize({
  theme: 'default',  // default, forest, dark, neutral
  startOnLoad: true
});
</script>
"))

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

machine api.github.com login geekard@qq.com password YOUR_TOKEN

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

  • M-x grip-start-preview
  • M-x grip-stop-preview
  • M-x grip-restart-preview
  • M-x grip-browse-preview 使用浏览器来预览
(use-package grip-mode
  :ensure-system-package
  (grip . "pip install grip")
  :bind
  (:map markdown-mode-command-map ("g" . grip-mode))
  :config
  (setq grip-preview-use-webkit nil)
  ;; 支持网络访问(默认 localhost)。
  (setq grip-preview-host "0.0.0.0")
  ;; 保存文件时才更新预览。
  (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))))

为 markdown 文件添加目录:

(use-package markdown-toc
  :after(markdown-mode)
  :bind (:map markdown-mode-command-map
              ("r" . markdown-toc-generate-or-refresh-toc)))

dockerfile

(use-package dockerfile-mode
  :ensure-system-package
  (docker-langserver . "npm install -g dockerfile-language-server-nodejs")
  :config
  (add-to-list 'auto-mode-alist '("Dockerfile\\'" . dockerfile-mode)))

web

typescript

(defun my/use-eslint-from-node-modules ()
  ;; use local eslint from node_modules before global
  ;; http://emacs.stackexchange.com/questions/21205/flycheck-with-file-relative-eslint-executable
  (let* ((root (locate-dominating-file (or (buffer-file-name) default-directory) "node_modules"))
         (eslint (and root (expand-file-name "node_modules/eslint/bin/eslint.js" root))))
    (when (and eslint (file-executable-p eslint))
      (setq-local flycheck-javascript-eslint-executable eslint))))

;; (shell-command "which npm &>/dev/null || brew install npm &>/dev/null")
(defun my/setup-tide-mode ()
  "Use hl-identifier-mode only on js or ts buffers."
  (when (and (stringp buffer-file-name)
             (string-match "\\.[tj]sx?\\'" buffer-file-name))
    (tide-setup)
    (add-hook 'flycheck-mode-hook #'my/use-eslint-from-node-modules)
    (tide-hl-identifier-mode +1)))

;; for .ts and .tsx file
(use-package typescript-mode
  :ensure-system-package
  (eslint . "npm install -g eslint babel-eslint eslint-plugin-react")
  :init
  (add-to-list 'auto-mode-alist '("\\.tsx?\\'" . typescript-mode))
  :hook
  ((typescript-mode . my/setup-tide-mode))
  :config
  (flycheck-add-mode 'typescript-tslint 'typescript-mode)
  (setq typescript-indent-level 2))
  • 安装 eslint npm 包后,安装语言服务器 M-x lsp-install-server RET eslint RET

tide 是 typescript/javascript 交互式开发环境,支持 js-mode(Emacs 27 内置)、js2-mode、web-mode(编辑模板文件, 如 HTML、Go Template 等)、typescript-mode。另外 jsts-ls(javascript-typescript-stdio) 不再维护了。

通过调用 ts-ls(npm install -g typescript-language-server)语言服务器,结合 company和 lsp 为 js/ts 提供代码补全 和导航。

(use-package tide
  :hook ((before-save . tide-format-before-save))
  :ensure-system-package
  ((typescript-language-server . "npm install -g typescript-language-server")
  (tsc . "npm install -g typescript"))
  :config
  ;; 开启 tsserver 的 debug 日志模式。
  (setq tide-tsserver-process-environment '("TSS_LOG=-level verbose -file /tmp/tss.log")))

js2-mode

js-mode (Emacs 27 内置) 和 js2-mode (js-mode 的增强,主要是 jsx 相关)用于编辑.js 和 .jsx 文件。

js-mode in Emacs 27 includes full support for syntax highlighting and indenting of JSX syntax. The currently recommended solution is to install Emacs 27 and use js-mode as the major mode. To make use of the JS2 AST and the packages that integrate with it, we recommend js2-minor-mode.

  • 继续使用 Emacs 自带的 js-mode, js2 作为 minor mode 来使用。
(use-package js2-mode
  :after (tide flycheck)
  :config
  ;; js-mode-map 将 M-. 绑定到 js-find-symbol, 没有使用 tide 和 lsp, 所以需要解
  ;; 绑。这样 M-. 被 tide 绑定到 tide-jump-to-definition.
  (define-key js-mode-map (kbd "M-.") nil)
  ;;(add-to-list 'auto-mode-alist '("\\.js\\'" . js2-mode))
  (add-hook 'js-mode-hook 'js2-minor-mode)
  ;; 为 js/jsx 文件启动 tide.
  (add-hook 'js-mode-hook 'my/setup-tide-mode)
  ;; disable jshint since we prefer eslint checking
  (setq-default flycheck-disabled-checkers (append flycheck-disabled-checkers '(javascript-jshint)))
  (flycheck-add-mode 'javascript-eslint 'js-mode)
  (flycheck-add-next-checker 'javascript-eslint 'javascript-tide 'append)
  (flycheck-add-next-checker 'javascript-eslint 'jsx-tide 'append)
  (add-to-list 'interpreter-mode-alist '("node" . js2-mode)))

web-mode

web-mode 用于编辑 html/css/jinja2/gotmpl/tmpl 等模板文件,不用于编辑 js/jsx/ts/tsx 等类型文件。

(use-package web-mode
  :after (flycheck)
  :init
  (add-to-list 'auto-mode-alist '("\\.jinja2?\\'" . web-mode))
  (add-to-list 'auto-mode-alist '("\\.css?\\'" . web-mode))
  (add-to-list 'auto-mode-alist '("\\.html?\\'" . web-mode))
  (add-to-list 'auto-mode-alist '("\\.tmpl\\'" . web-mode))
  (add-to-list 'auto-mode-alist '("\\.gotmpl\\'" . web-mode))
  :custom
  (web-mode-enable-auto-pairing t)
  (web-mode-enable-css-colorization t)
  (web-mode-markup-indent-offset 4)
  (web-mode-css-indent-offset 4)
  (web-mode-code-indent-offset 4)
  (web-mode-enable-auto-quoting nil)
  (web-mode-enable-block-face t)
  (web-mode-enable-current-element-highlight t)
  :config
  (flycheck-add-mode 'javascript-eslint 'web-mode))

(defun my/json-format ()
  (interactive)
  (save-excursion
    (shell-command-on-region (mark) (point) "python -m json.tool" (buffer-name) t)))

yaml

(use-package yaml-mode
  :ensure-system-package
  (yaml-language-server . "npm install -g yaml-language-server")
  :hook
  (yaml-mode . (lambda () (define-key yaml-mode-map "\C-m" 'newline-and-indent)))
  :config
  (add-to-list 'auto-mode-alist '("\\.yml\\'" . yaml-mode))
  (add-to-list 'auto-mode-alist '("\\.yaml\\'" . yaml-mode)))

direnv

目录变量是只对特定目录及子目录有效的 shell 环境变量,主要使用场景是根据目录变量来指定版本的 python 和 golang 。

先安装 direnv 命令 brew install direnv, 然后在 ~/.bashrc 中添加:

eval "$(direnv hook bash)"

安装 emacs envrc 软件包,它调用 direnv 命令获取当前文件或目录的环境变量,然后更新 emacs 变量 process-environmentexec-path ,这样 emacs 后续启动的命令就会继承这些环境变量(direnv 包是全局的,而 envrc 是 buffer-local 的):

(use-package envrc
  :ensure-system-package direnv
  :hook (after-init . envrc-global-mode)
  :config
  (define-key envrc-mode-map (kbd "C-c e") 'envrc-command-map))
  • C-c e a: envrc-allow
  • C-c e d: envrc-deny
  • C-c e r: envrc-reload

使用步骤:

  1. 在对应目录创建 .envrc 文件;
  2. 向 .envrc 文件添加 shell 环境变量;
  3. 执行 direnv allow . 生效环境变量;

如果某些变量未被 lsp 识别,则需要打开 .envrc 所在目录的文件后执行 M-x lsp-workspace-restart 来重启 lsp 。

others

;; 移动到行或代码的开头、结尾。
(use-package mwim
  :bind (([remap move-beginning-of-line] . mwim-beginning-of-code-or-line)
         ([remap move-end-of-line] . mwim-end-of-code-or-line)))

;; 智能括号。
(use-package smartparens
  :demand
  :config
  (smartparens-global-mode t)
  (show-smartparens-global-mode t))

;; 彩色括号。
(use-package rainbow-delimiters
  :hook
  (prog-mode . rainbow-delimiters-mode))

;; 高亮匹配的括号。
(use-package paren
  :straight (:type built-in)
  :hook
  (after-init . show-paren-mode)
  :init
  (setq show-paren-when-point-inside-paren t
        show-paren-when-point-in-periphery t))

;; 开发文档。
(use-package dash-at-point
  :bind
  (("C-c d ." . dash-at-point)
   ("C-c d d" . dash-at-point-with-docset)))

expand-region

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

project

(defun my/project-try-local (dir)
  "Determine if DIR is a non-Git project."
  (catch 'ret
    (let ((pr-flags '((".project")
                      ("go.mod" "pom.xml" "package.json") ;; higher priority
                      ("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-info ()
  (interactive)
  (message "%s" (project-current t)))

(defun my/project-add (dir)
  (interactive "DDirectory: \n")
  ;; 使用 project-remember-project 报错。
  (project-remember-projects-under dir nil))

(defun my/project-new-root ()
  (interactive)
  (let* ((root-dir (read-directory-name "Root: "))
         (f (expand-file-name ".project" root-dir)))
    (message "Create %s..." f)
    (make-directory root-dir t)
    (when (not (file-exists-p f))
      (make-empty-file f))
    (my/project-add root-dir)))

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

(defun my/project-discover ()
  (interactive)
  (dolist (search-path '("~/go/src/github.com/*" "~/go/src/github.com/*" "~/go/src/k8s.io/*" "~/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)))))

treemacs

(use-package treemacs
  :demand
  :straight 
  (treemacs :files ("src/elisp/*.el" "src/scripts/*.py" "src/extra/*.el" "icons")  :repo "Alexander-Miller/treemacs")
  :init
  (with-eval-after-load 'winum (define-key winum-keymap (kbd "M-0") #'treemacs-select-window))
  :config
  (progn
    (setq
     treemacs-collapse-dirs                 3
     treemacs-deferred-git-apply-delay      0.1
     treemacs-display-in-side-window        t
     treemacs-eldoc-display                 t
     treemacs-file-event-delay              500
     treemacs-file-follow-delay             0.01
     treemacs-follow-after-init             t
     treemacs-git-command-pipe              ""
     treemacs-goto-tag-strategy             'refetch-index
     treemacs-indentation                   1
     treemacs-indentation-string            " "
     treemacs-is-never-other-window         t
     treemacs-max-git-entries               100
     treemacs-missing-project-action        'ask
     treemacs-no-png-images                 nil
     treemacs-no-delete-other-windows       t
     treemacs-persist-file                  (expand-file-name ".cache/treemacs-persist" user-emacs-directory)
     treemacs-position                      'left
     treemacs-recenter-distance             0.01
     treemacs-recenter-after-file-follow    t
     treemacs-recenter-after-tag-follow     t
     treemacs-recenter-after-project-jump   'always
     treemacs-recenter-after-project-expand 'on-distance
     treemacs-shownn-cursor                 t
     treemacs-show-hidden-files             t
     treemacs-silent-filewatch              nil
     treemacs-silent-refresh                nil
     treemacs-sorting                       'alphabetic-asc
     treemacs-select-when-already-in-treemacs 'stay
     treemacs-space-between-root-nodes      nil
     treemacs-tag-follow-cleanup            t
     treemacs-tag-follow-delay              1
     treemacs-width                         35
     treemacs-width-increment               5
     treemacs-width-is-initially-locked     nil
     treemacs-project-follow-cleanup        t
     imenu-auto-rescan                      t)
    (treemacs-resize-icons 11)
    (treemacs-follow-mode t)
    ;;(treemacs-tag-follow-mode t)
    ;;(treemacs-project-follow-mode t)
    (treemacs-filewatch-mode t)
    (treemacs-fringe-indicator-mode 'always)
    ;; 在 modus-theme 下显示不好, 故关闭。
    ;;(treemacs-indent-guide-mode t)
    (treemacs-git-mode 'deferred)
    (treemacs-hide-gitignored-files-mode nil))
  ;; 使用 treemacs 自带的 all-the-icons 主题。
  ;; 注: 当使用 doom-themes 主题时, 它会自动设置 treemacs theme, 就不需要再调用这个函数了.
  ;;(require 'treemacs-all-the-icons)
  ;;(treemacs-load-theme "all-the-icons")
  ;;(require 'treemacs-projectile)
  (require 'treemacs-magit)
  ;; 在 dired buffer 中使用 treemacs icons。
  (require 'treemacs-icons-dired)
  (treemacs-icons-dired-mode t)
  ;; 单击打开或折叠目录.
  (define-key treemacs-mode-map [mouse-1] #'treemacs-single-click-expand-action)
  :bind
  (:map global-map
        ("M-0"       . treemacs-select-window)
        ("C-x t 1"   . treemacs-delete-other-windows)
        ("C-x t t"   . treemacs)
        ("C-x t B"   . treemacs-bookmark)
        ("C-x t C-t" . treemacs-find-file)
        ("C-x t M-t" . treemacs-find-tag)))

;; C-c p s r(projectile-ripgrep) 依赖 ripgrep 包。
(use-package ripgrep
  :ensure-system-package (rg . ripgrep))

(use-package deadgrep
  :ensure-system-package (rg . ripgrep)
  :bind ("<f5>" . deadgrep))

;; 为远程 buffer 关闭 treemacs, 避免建立新连接耗时。
(add-hook 'buffer-list-update-hook
          (lambda ()
            (when (file-remote-p default-directory)
              (require 'treemacs)
              (if (string-match "visible" (symbol-name (treemacs-current-visibility)))
                  (delete-window (treemacs-get-local-window))))))

;; lsp-treemacs 显示 lsp workspace 文件夹和 treemacs projects 。
(use-package lsp-treemacs
  :after (lsp-mode treemacs)
  :config
  (setq lsp-treemacs-error-list-current-project-only t)
  (lsp-treemacs-sync-mode 1))
  • 不启用 (treemacs-project-follow-mode t), 否则每次 treemacs 只会显示一个 project, 对于 Go 等有多个 module project 的情况不友好。不启用后,可以按需手动将 Go 依赖的 module project 加到当前 workspace 中。

dict

;; OSX 词典。
(use-package osx-dictionary
  :bind
  (("C-c t i" . osx-dictionary-search-input)
   ("C-c t w" . osx-dictionary-search-pointer))
  :config
  (use-package chinese-word-at-point :demand t)
  (setq osx-dictionary-use-chinese-text-segmentation t))

search

设置 grep/ripgrep 忽略的目录和文件:

(require 'grep)
(dolist (dir '(".cache" "vendor" "node_modules"))
  (add-to-list 'grep-find-ignored-directories dir))
(dolist (item '("GPATH" "GRTAGS" "GTAGS" "TAGS" ".classpath" ".project" ".DS_Store" ))
  (add-to-list 'grep-find-ignored-files item))

ctrlf 是 isearch 更好的替代品(搜索结果编号,支持: C-v, M-v, M-<, M->, C-l):

;; 当前 buffer 文本搜索, 替换 isearch.
(use-package ctrlf
  :config
  (ctrlf-mode +1)
  (add-hook 'pdf-isearch-minor-mode-hook (lambda () (ctrlf-local-mode -1))))

在线搜索:

  • 搜索前缀命令: C-c s , 可以先选中 region 再执行上面的搜索。
  • 修复启动报错: rm ~/.emacs.d/elpa/engine-mode*/engine-mode-*.el*
;; browser-url 使用 Mac 默认浏览器。
(setq browse-url-browser-function 'browse-url-default-macosx-browser)

(use-package engine-mode
  :config
  (engine/set-keymap-prefix (kbd "C-c s"))
  (engine-mode t)
  ;;(setq engine/browser-function 'eww-browse-url)
  (defengine github
    "https://github.com/search?ref=simplesearch&q=%s"
    :keybinding "h")

  (defengine google
    "http://www.google.com/search?ie=utf-8&oe=utf-8&q=%s"
    :keybinding "g")

  (defengine twitter
    "https://twitter.com/search?q=%s"
    :keybinding "t")

  (defengine wikipedia
    "http://www.wikipedia.org/search-redirect.php?language=en&go=Go&search=%s"
    :keybinding "w"
    :docstring "Searchin' the wikis."))

proxy

全局 socks5 代理:

  • Mac 自带的 curl 不支持 socks 代理, 需要安装 brew install curl 并设置 ~export PATH=”/usr/local/opt/curl/bin:$PATH”~
  • url-retrieve 使用 curl 作为后端实现, 这样全局可使用 socks5 代理。
  • 需要添加 --user-agent 配置, 否则会被 Google 403 Forbidden;
;; 添加环境变量 export PATH="/usr/local/opt/curl/bin:$PATH"
(use-package emacs
  :straight (:type built-in)
  :ensure-system-package ("/usr/local/opt/curl/bin/curl" . "brew install curl"))

(setq my/socks-host "127.0.0.1")
(setq my/socks-port 13659)
(setq my/socks-proxy (format "socks5h://%s:%d" my/socks-host my/socks-port))

(use-package mb-url-http
  :demand
  :straight (mb-url :repo "dochang/mb-url")
  :commands (mb-url-http-around-advice)
  :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 "/usr/local/opt/curl/bin/curl"
          mb-url-http-curl-switches `("-k" "-x" ,my/socks-proxy
                                      ;;"--max-time" "300"
                                      ;;"-u" ,github-auth
                                      ;;"--user-agent" "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"
                                      ))))

(defun proxy-socks-show ()
  "Show SOCKS proxy."
  (interactive)
  (when (fboundp 'cadddr)
    (if (bound-and-true-p socks-noproxy)
        (message "Current SOCKS%d proxy is %s:%d" 5 my/socks-host my/socks-port)
      (message "No SOCKS proxy"))))

(defun proxy-socks-enable ()
  "使用 socks 代理 url 访问请求。"
  (interactive)
  (require 'socks)
  (setq url-gateway-method 'socks
        socks-noproxy '("localhost" "10.0.0.0/8" "172.0.0.0/8" "*cn" "*alibaba-inc.com" "*taobao.com" "*antfin-inc.com")
        socks-server `("Default server" ,my/socks-host ,my/socks-port 5))
  (setenv "all_proxy" my/socks-proxy)
  (proxy-socks-show)
  ;;url-retrieve 使用 curl 作为后端实现, 支持全局 socks5 代理。
  (advice-add 'url-http :around 'mb-url-http-around-advice))

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

(defun proxy-socks-toggle ()
  "Toggle SOCKS proxy."
  (interactive)
  (require 'socks)
  (if (bound-and-true-p socks-noproxy)
      (proxy-socks-disable)
    (proxy-socks-enable)))

terminal

(use-package vterm
  :ensure-system-package
  ((cmake . cmake)
   (glibtool . libtool)
   (exiftran . exiftran))
  :config
  (setq vterm-set-bold-hightbright t)
  (setq vterm-always-compile-module t)
  (setq vterm-max-scrollback 100000)
  ;; vterm buffer 名称,需要配置 shell 来支持(如 bash 的 PROMPT_COMMAND)。
  (setq vterm-buffer-name-string "*vterm: %s")
  (add-hook 'vterm-mode-hook
            (lambda ()
              (setf truncate-lines nil)
              (setq-local show-paren-mode nil)
              (yas-minor-mode -1)
              (flycheck-mode -1)))
  ;; 使用 M-y(consult-yank-pop) 粘贴剪贴板历史中的内容。
  (define-key vterm-mode-map [remap consult-yank-pop] #'vterm-yank-pop)
  :bind
  (:map vterm-mode-map ("C-l" . nil))
  ;; 防止输入法切换冲突。
  (:map vterm-mode-map ("C-\\" . nil)) )

(use-package multi-vterm
  :after (vterm)
  :config
  (define-key vterm-mode-map (kbd "M-RET") 'multi-vterm))

(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-~") 'vterm-toggle-cd)
  (define-key vterm-mode-map (kbd "C-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))

(use-package vterm-extra
  :straight (:host github :repo "Sbozzolo/vterm-extra")
  :bind (("s-t" . vterm-extra-dispatcher)
         :map vterm-mode-map
         (("C-c C-e" . vterm-extra-edit-command-in-new-buffer)))
  :config
  (advice-add #'vterm-extra-edit-done :after #'winner-undo))
  • vterm-extra 提供了 vterm buffer 命令行编辑的能力,结束后按 C-c C-c 自动粘贴到对应的 vterm 中,与 consult-yasnippet 结合使用,可以实现在 vterm 里粘贴 yasnippet 的功能。

eshell 配置:

(setq explicit-shell-file-name "/bin/bash")
(setq shell-file-name "/bin/bash")
(setq shell-command-prompt-show-cwd t)
(setq explicit-bash.exe-args '("--noediting" "--login" "-i"))
(setenv "SHELL" shell-file-name)
(setenv "ESHELL" "bash")
(add-hook 'comint-output-filter-functions 'comint-strip-ctrl-m)

;; 提示符只读
(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)

tramp

(use-package tramp
  :straight (tramp :files ("lisp/*"))
  :config
  ;; 使用 ~/.ssh/config 中的 ssh 持久化配置。(Emacs 默认复用连接,但不持久化连接)
  (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))
  ;; 调大远程文件名过期时间(默认 10s), 提高查找远程文件性能.
  (setq remote-file-name-inhibit-cache 600)
  ;;tramp-verbose 10
  ;; 增加压缩传输的文件起始大小(默认 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 1024 2))
  ;; 临时目录中保存 TRAMP auto-save 文件, 重启后清空。
  (setq tramp-allow-unsafe-temporary-files t)
  (setq tramp-auto-save-directory temporary-file-directory)
  ;; 连接历史文件。
  (setq tramp-persistency-file-name (expand-file-name "tramp-connection-history" user-emacs-directory))
  ;; 在整个 Emacs session 期间保存 SSH 密码.
  (setq password-cache-expiry nil)
  (setq tramp-default-method "ssh")
  (setq tramp-default-remote-shell "/bin/bash")
  (setq tramp-default-user "root")
  (setq tramp-terminal-type "tramp")
  ;; 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))

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

;; 远程机器列表。
(require 'epa-file)
(epa-file-enable)
(load "~/.emacs.d/sshenv.el.gpg")

;; 切换 buffer 时自动设置 VTERM_HOSTNAME 环境变量为多跳的最后一个主机名,并通过 vterm-environment 传递到远程环境中。远程
;; 机器的 ~/.emacs_bashrc 根据这个变量设置 Buffer 名称和机器访问地址为主机名,正确设置目录跟踪。解决多跳时 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
  :straight (:repo "Ladicle/consult-tramp" :host github))
  • tramp-default-method 缺省值为 scp, 不支持多跳(但拷贝大文件时性能更高),再打开多跳远程文件时每次都需要修改 /- 中的 -为 ssh,较麻烦,所以设置为 ssh。

others

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

(setq initial-major-mode 'fundamental-mode)

;; 按中文折行。
(setq word-wrap-by-category t)

;; 编辑 grep buffers, 可以和 consult-grep 和 embark-export 联合使用。
(use-package wgrep)
(setq grep-highlight-matches t)

;; 退出自动杀掉进程。
(setq confirm-kill-processes nil)

;; 使用 fundamental-mode 打开大文件。
(defun my/large-file-hook ()
  (when (and (> (buffer-size) (* 1024 2))
             (or (string-equal (file-name-extension (buffer-file-name)) "json")
                 (string-equal (file-name-extension (buffer-file-name)) "yaml")
                 (string-equal (file-name-extension (buffer-file-name)) "yml")
                 (string-equal (file-name-extension (buffer-file-name)) "log")))
    (fundamental-mode)
    (setq buffer-read-only t)
    (font-lock-mode -1)
    (rainbow-delimiters-mode -1)
    (smartparens-global-mode -1)
    (show-smartparens-mode -1)
    (smartparens-mode -1)))
(add-hook 'find-file-hook 'my/large-file-hook)
;; 默认直接用 fundamental-mode 打开 json 和 log 文件, 确保其它 major-mode 不会先执行。
(add-to-list 'auto-mode-alist '("\\.log?\\'" . fundamental-mode))
(add-to-list 'auto-mode-alist '("\\.json?\\'" . fundamental-mode))

(use-package winner
  :straight (:type built-in)
  :commands (winner-undo winner-redo)
  :hook (after-init . winner-mode)
  :init
  (setq winner-boring-buffers
        '("*Completions*"
          "*Compile-Log*"
          "*inferior-lisp*"
          "*helpful"
          "*lsp-help*"
          "*Fuzzy Completions*"
          "*Apropos*"
          "*Help*"
          "*cvs*"
          "*Buffer List*"
          "*Ibuffer*"
          "*esh command on file*")))

;; macOS 按键调整。
(setq mac-command-modifier 'meta)
;; option 作为 Super 键(按键绑定用 s- 表示,S- 表示 Shift)
(setq mac-option-modifier 'super)
;; fn 作为 Hyper 键(按键绑定用 H- 表示)
(setq ns-function-modifier 'hyper)

(use-package emacs
  :straight (:type built-in)
  :init
  (setq use-short-answers t)
  (setq confirm-kill-emacs #'y-or-n-p)
  (setq ring-bell-function 'ignore)
  ;; 不创建 lock 文件。
  (setq create-lockfiles nil)
  ;; 启动 Server 。
  (unless (and (fboundp 'server-running-p)
               (server-running-p))
    (server-start)))

;; bookmark 发生变化时自动保存(默认是 Emacs 正常退出时保存)。
(setq bookmark-save-flag 1)

;; 避免执行 ns-print-buffer 命令。
(global-unset-key (kbd "s-p"))
;; 避免执行 ns-open-file-using-panel 命令。
(global-unset-key (kbd "s-o"))
(global-unset-key (kbd "s-t"))
(global-unset-key (kbd "s-n"))
;; 关闭 suspend-frame 。
(global-unset-key (kbd "C-z"))
;; 关闭 mouse-yank-primary 。
(global-unset-key (kbd "<mouse-2>"))

(use-package recentf
  :straight (:type built-in)
  :config
  ;; 不清理 recentf tramp buffers .
  (setq recentf-auto-cleanup 'never)
  (setq recentf-max-menu-items 30)
  (setq recentf-max-saved-items 500)
  (setq recentf-exclude `(,(expand-file-name "straight/" user-emacs-directory)
                          ,(expand-file-name "eln-cache/" user-emacs-directory)
                          ,(expand-file-name "etc/" user-emacs-directory)
                          ,(expand-file-name "var/" user-emacs-directory)
                          ,(expand-file-name ".cache/" user-emacs-directory)                          
                          ,tramp-file-name-regexp
                          "/tmp" ".gz" ".tgz" ".xz" ".zip" "/ssh:" ".png" ".jpg" "/\\.git/" ".gitignore" "\\.log" "COMMIT_EDITMSG"
                          ,(concat package-user-dir "/.*-autoloads\\.el\\'")))
  (recentf-mode +1))

(setq global-mark-ring-max 5000)
(setq mark-ring-max 5000 )
(setq kill-ring-max 5000)

(global-set-key (kbd "RET") 'newline-and-indent)

;; Minibuffer 历史。
(use-package savehist
  :straight (:type built-in)
  :hook (after-init . savehist-mode)
  :config
  (setq history-length 200)
  (setq savehist-save-minibuffer-history t)
  (setq savehist-autosave-interval 200)
  (setq savehist-additional-variables '(mark-ring
                                        global-mark-ring
                                        search-ring
                                        regexp-search-ring
                                        extended-command-history)))

;; fill-column 的值应该小于 visual-fill-column-width,否则居中显示时行内容会过长而被隐藏。
(setq-default fill-column 100)
(setq-default comment-fill-column 0)
(setq-default tab-width 4)
;; 不插入 tab (按照 tab-width 转换为空格插入) 。
(setq-default indent-tabs-mode nil)
(setq-default message-log-max t)
(setq-default ad-redefinition-action 'accept)

;; 使用系统剪贴板,实现与其它程序相互粘贴。
(setq x-select-enable-clipboard t)
(setq select-enable-clipboard t)
(setq x-select-enable-primary t)
(setq select-enable-primary t)

;; 粘贴于光标处, 而不是鼠标指针处。
(setq mouse-yank-at-point t)

(use-package ibuffer
  :straight (:type built-in)
  :bind
  ("C-x C-b" . ibuffer)
  :config
  (setq ibuffer-expert t)
  (setq ibuffer-display-summary nil)
  (setq ibuffer-use-other-window t)
  ;; 隐藏空组。
  (setq ibuffer-show-empty-filter-groups nil)
  (setq ibuffer-movement-cycle nil)
  (setq ibuffer-default-sorting-mode 'filename/process)
  (setq ibuffer-use-header-line t)
  (setq ibuffer-default-shrink-to-minimum-size nil)
  (setq ibuffer-saved-filter-groups nil)
  (setq ibuffer-old-time 48)
  (add-hook 'ibuffer-mode-hook #'hl-line-mode))

(use-package ibuffer-project
  :hook
  ((ibuffer . (lambda ()
                (setq ibuffer-filter-groups (ibuffer-project-generate-filter-groups))
                (unless (eq ibuffer-sorting-mode 'project-file-relative)
                  (ibuffer-do-sort-by-project-file-relative)))))
  :config
  ;; 显示的文件名是相对于 project root 的相对路径。
  (setq ibuffer-formats
        '((mark modified read-only " "
                (name 18 18 :left :elide)
                " "
                (size 9 -1 :right)
                " "
                (mode 16 16 :left :elide)
                " "
                project-file-relative))))

(use-package dired
  :straight (:type built-in)
  :config
  ;; re-use dired buffer, available in Emacs 28
  ;; @see https://debbugs.gnu.org/cgi/bugreport.cgi?bug=20598
  (setq dired-kill-when-opening-new-dired-buffer t)
  ;; "always" means no asking
  (setq dired-recursive-copies 'always)
  ;; "top" means ask once for top level directory
  (setq dired-recursive-deletes 'top)
  ;; search file name only when focus is over file
  (setq dired-isearch-filenames 'dwim)
  ;; if another Dired buffer is visible in another window, use that directory as target for Rename/Copy
  (setq dired-dwim-target t)
  ;; @see https://emacs.stackexchange.com/questions/5649/sort-file-names-numbered-in-dired/5650#5650
  (setq dired-listing-switches "-laGh1v --group-directories-first")
  (dired-async-mode 1)
  (put 'dired-find-alternate-file 'disabled nil))

;; dired 显示高亮增强。
(use-package diredfl
  :config
  (diredfl-global-mode))

(use-package undo-tree
  :init
  (global-undo-tree-mode 1))

(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)
(setq backup-directory-alist (list (cons ".*" 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)))
;;(global-auto-revert-mode)

;; UTF8 中文字符。
(setq locale-coding-system 'utf-8)
(setq default-buffer-file-coding-system 'utf-8)
(set-default buffer-file-coding-system 'utf8)
(prefer-coding-system 'utf-8)
(set-buffer-file-coding-system 'utf-8)
(set-language-environment "UTF-8")
(set-default-coding-systems 'utf-8)
(setenv "LANG" "zh_CN.UTF-8")
(setenv "LC_ALL" "zh_CN.UTF-8")
(setenv "LC_CTYPE" "zh_CN.UTF-8")

;; 删除文件时, 将文件移动到回收站。
(use-package osx-trash
  :ensure-system-package 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))

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

;; 以下自定义函数参考自:https://github.com/jiacai2050/dotfiles/blob/master/.config/emacs/i-edit.el
(defun my/other-window-backward ()
  "Goto previous window"
  (interactive)
  (other-window -1))

(defun my/open-terminal ()
  "Open system terminal."
  (interactive)
  (cond
   ((eq system-type 'darwin)
    (shell-command
     ;; open -a Terminal doesn't allow us to open a particular directory unless
     ;; We use --args AND -n, but -n opens an entirely new Terminal application
     ;; instance on every call, not just a new window. Using the
     ;; bundle here always opens the given directory in a new window.
     (concat "open -b com.apple.terminal " default-directory) nil nil))
   ((memq system-type '(cygwin windows-nt ms-dos))
    ;; https://stackoverflow.com/questions/13505113/how-to-open-the-native-cmd-exe-window-in-emacs
    (let ((proc (start-process "cmd" nil "cmd.exe" "/C" "start" "cmd.exe")))
      (set-process-query-on-exit-flag proc nil)))
   (t
    (message "Implement `j-open-terminal' for this OS!"))))

(defun my/iso-8601-date-string (&optional datetime)
  (concat
   (format-time-string "%Y-%m-%dT%T" datetime)
   ((lambda (x) (concat (substring x 0 3) "" (substring x 3 5)))
    (format-time-string "%z" datetime))))

(defun my/insert-current-date-time ()
  (interactive)
  (insert (my/iso-8601-date-string)))

(defun my/insert-today ()
  (interactive)
  (insert (format-time-string "%Y-%m-%d" (current-time))))

(defun my/timestamp->human-date ()
  (interactive)
  (letrec ((date-string (if (region-active-p)
                            (buffer-substring (mark) (point))
                          (thing-at-point 'word)))
           (body (if (iso8601-valid-p date-string)
                     ;; date -> ts
                     (format-time-string "%s" (parse-iso8601-time-string date-string))
                   ;; ts -> date
                   (let ((timestamp-int (string-to-number date-string)))
                     (thread-last
                       (if (> timestamp-int (expt 10 11)) ;; 大于 10^11 为微秒,转为秒
                           (/ timestamp-int 1000)
                         timestamp-int)
                       (seconds-to-time)
                       (my/iso-8601-date-string))))))
    (unless (string-empty-p body)
      (end-of-line)
      (newline-and-indent)
      (insert body))
    (deactivate-mark)))

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

(defun my/rename-current-buffer-file ()
  "Renames current buffer and file it is visiting."
  (interactive)
  (let ((name (buffer-name))
        (filename (buffer-file-name)))
    (if (not (and filename (file-exists-p filename)))
        (error "Buffer '%s' is not visiting a file!" name)
      (let ((new-name (read-file-name "New name: ")))
        (if (get-buffer new-name)
            (error "A buffer named '%s' already exists!" new-name)
          (rename-file filename new-name 1)
          (rename-buffer new-name)
          (set-visited-file-name new-name)
          (set-buffer-modified-p nil)
          (message "File '%s' successfully renamed to '%s'"
                   name (file-name-nondirectory new-name)))))))

(defun my/mem-report ()
  (interactive)
  (let ((max-lisp-eval-depth 10000000)
        (max-specpdl-size 10000000))
    (memory-report)))

(use-package emacs
 :straight (:type built-in)
 :ensure-system-package
 ;; artist-mode 依赖的两个程序。
 ((figlet . "brew install figlet")
  ;; 触摸板三指点按模拟鼠标中键。
  ("/Applications/MiddleClick.app" . "brew install --cask --no-quarantine middleclick"))
 :config
 ;; 不显示行号, 否则鼠标会飘。
 (add-hook 'artist-mode-hook (lambda () (display-line-numbers-mode -1))))
  • osx-trash 不支持 TRAMP 删除远程文件,解决办法:用 %m 标记文件,然后按 ! 执行 rm 命令。

refs

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

  1. seagle0128/.emacs.d
  2. protesilaos/dotfiles
  3. bbatsov/prelude
  4. MatthewZMD/.emacs.d
  5. condy0919/.emacs.d
  6. manateelazycat/lazycat-emacs
  7. jiacai2050/dotfiles
  8. natecox/dotfiles
  9. daviwil

About

Make emacs powerful forever!


Languages

Language:Emacs Lisp 94.6%Language:TeX 3.5%Language:YASnippet 1.3%Language:Common Lisp 0.6%