Paradigm X

Vision quests of a soulhacker

Doom Emacs 配置

这份配置基于 Emacs 模式的按键组合,没有启用 Doom Emacs 缺省的 evil 模式,如果希望使用类 Vim 的按键方式,可以参考其他一些大牛,比如的 Tec Osaur 的非常有教益的版本。

前言

Doom Emacs 的配置都在变量 doom-user-dir 指向的目录下(根据安装时的选择可能是 ~/.doom.d/~/.config/doom/ ),主要是三个文件:

  • init.el :启用/停用 Doom 预先配置好的模块( modules );
  • package.el :自己另行下载安装的软件包( packages ),或对已启用的软件包版本进行配置,Doom Emacs 使用 straight.el 包管理器
  • config.el :各种配置,大部分预制模块不需要或者只需要少许配置,因为 Doom 已经做了相对好的缺省安排,自行安装的包则需要做相应的配置;当然如果对 Doom 提供的缺省安排不满意,也可以在此动手覆盖。

要更改配置就编辑这三个文件即可,但我们有更好的选择,那就是 literate config ,这是个很酷的功能 1,我们把所有配置代码都用 org-babel 的源码块( source block )写在这个 config.org 文件中(对,你现在看到的就是这个文件本身),然后用 org-babel 提供的 org-babel-tangle-file 函数来自动生成上述三个文件。

emacs --batch --eval "(progn (require 'org) (setq org-confirm-babel-evaluate nil) (org-babel-tangle-file \"~/.config/doom/config.org\"))"

这个 bootstrap 过程只需要做一次,之后 Doom 提供的 literate 模块会自动维护。

无论通过什么方式,配置文件变化后要使用 doom sync 命令来更新环境(这个命令首先会自动从本文件出发重新构造新的配置文件),还可以用 doom doctor 来检查配置中可能存在的问题(最常见的问题是依赖的外部程序找不到或者没配好);Doom Emacs 更新比较频繁,而且鼓励用户滚动更新紧跟开发分支,所以可以定期用 doom upgrade 来更新 Doom Emacs 本身及所有启用的包。

本着不折腾的原则,我对功能实现方案总的选择标准是:

  • 能用预制模块的尽量用;
  • 能用缺省配置的尽量用;
  • 重视持续维护和移植的代价;
  • 还是不喜欢 evil

预制模块

Doom 提供的大量预制模块大多是当前优选,难以确定的地方也提供了主流方案多选一,并进行了比较恰当的配置,是宝藏,我的配置主要以这些预制模块为主。

如果不清楚某个模块效果到底如何,可以启用试试;官方文档虽然还不完整,但已经挺有用了,可以了解每个模块的基本配置项、依赖和一些常见问题,千万不要错过。

;;; init.el -*- lexical-binding: t; -*-

(doom! :input
       <<doom-input>>

       :completion
       <<doom-completion>>

       :ui
       <<doom-ui>>

       :editor
       <<doom-editor>>

       :emacs
       <<doom-emacs>>

       :term
       <<doom-term>>

       :checkers
       <<doom-checkers>>

       :tools
       <<doom-tools>>

       :os
       <<doom-os>>

       :lang
       <<doom-lang>>

       :email
       <<doom-email>>

       :app
       <<doom-app>>

       :config
       <<doom-config>>
       )

基本设置

Doom 的 :config 缺省配置大多很好,少数需要定制的后面再说;另外很重要的是启用 literate 模块。

literate
(default +bindings +smartparens)

用户界面

这部分包含了 Doom 的 :completion :ui :editor :emacs :term :checkers :tools :os 几大类别。

无处不在的自动补全功能,选择最时髦的 vertico

(company +childframe)       ; the ultimate code completion backend
;;helm                      ; the *other* search engine for love and life
;;ido                       ; the other *other* search engine...
;;ivy                       ; a search engine for love and life
(vertico +icons)            ; the search engine of the future

这里面有偏视觉和娱乐的(前者如 hl-todo ligatures 后者如 doom-quit ),但大多数是很实用的,比如 window-select 大幅强化了多窗口时切换窗口 C-x o 的体验(实际使用的是 ace-window )。另外 unicode 这个非常重要,如果不打开会有很多中文显示方面的问题。

;;deft                      ; notational velocity for Emacs
doom                        ; what makes DOOM look the way it does
doom-dashboard              ; a nifty splash screen for Emacs
;;doom-quit                 ; DOOM quit-message prompts when you quit Emacs
(emoji +unicode +github)    ; 🙂
hl-todo                     ; highlight TODO/FIXME/NOTE/DEPRECATED/HACK/REVIEW
;;hydra
;;indent-guides             ; highlighted indent columns
(ligatures +extra)          ; ligatures and symbols to make your code pretty again
;;minimap                   ; show a map of the code on the side
modeline                    ; snazzy, Atom-inspired modeline, plus API
nav-flash                   ; blink cursor line after big motions
;;neotree                   ; a project drawer, like NERDTree for vim
ophints                     ; highlight the region an operation acts on
(popup +all +defaults)      ; tame sudden yet inevitable temporary windows
;;tabs                      ; a tab bar for Emacs
(treemacs +lsp)             ; a project drawer, like neotree but cooler
unicode                     ; extended unicode support for various languages
(vc-gutter +pretty)         ; vcs diff in the fringe
;;vi-tilde-fringe           ; fringe tildes to mark beyond EOB
(window-select +number)     ; visually switch windows
workspaces                  ; tab emulation, persistence & separate workspaces
;;zen                       ; distraction-free coding or writing

编辑相关的模块这里,我做的最大选择是不用 evil ,当然很多 Spacemacs 和 Doom 用户都会选择这个模式。这里要特别提一下的是 lipsy ,这是针对 Lisp 类语言的一个编辑模式,非常巧妙的实现了单按键的光标快速移动和编辑操作,但不需要切 Vim 模式,我认为比传统的 pareditsmartparens 都更高明。

;;(evil +everywhere)        ; come to the dark side, we have cookies
file-templates              ; auto-snippets for empty files
fold                        ; (nigh) universal code folding
;;(format +onsave)          ; automated prettiness
;;god                       ; run Emacs commands without modifier keys
lispy                       ; vim for lisp, for people who don't like vim
multiple-cursors            ; editing in many places at once
;;objed                     ; text object editing for the innocent
;;parinfer                  ; turn lisp into python, sort of
;;rotate-text               ; cycle region at point between text candidates
snippets                    ; my elves. They type so I don't have to
word-wrap                   ; soft wrapping with language-aware indent

一些 Emacs 常规功能,基本都开启就行了。

(dired +dirvish +icons)     ; making dired pretty [functional]
electric                    ; smarter, keyword-based electric-indent
(ibuffer +icons)            ; interactive buffer management
undo                        ; persistent, smarter undo for your inevitable mistakes
vc                          ; version-control and Emacs, sitting in a tree

Doom 支持几种内置的终端模拟器,效果最好的应该是 vterm

;;eshell                    ; the elisp shell that works everywhere
;;shell                     ; simple shell REPL for Emacs
;;term                      ; basic terminal emulator for Emacs
vterm                       ; the best terminal emulation in Emacs

关于拼写检查,我试了各种组合,目前最好的应该还是 flyspellaspell

(syntax +childframe)        ; tasing you for every semicolon you forget
(spell +aspell +flyspell)   ; tasing you for misspelling mispelling
(grammar +lsp)              ; tasing grammar mistake every you make

这一组工具我基本都是缺省选择,值得一提的是 magit 真棒,用起来简直上瘾,连写 commit message 都变得香甜了。注意 lsp 要打开,后面配置很多编程语言时会用到。

;;ansible
;;biblio                    ; Writes a PhD for you (citation needed)
;;collab                    ; buffers with friends
(debugger +lsp)             ; FIXME stepping through code, to help you add bugs
direnv
;;docker
;;editorconfig              ; let someone else argue about tabs vs spaces
ein                         ; tame Jupyter notebooks with emacs
(eval +overlay)             ; run code, run (also, repls)
lookup                      ; navigate your code and its documentation
lsp                         ; M-x vscode
(magit +forge)              ; a git porcelain for Emacs
make                        ; run make tasks from Emacs
;;pass                      ; password manager for nerds
pdf                         ; pdf enhancements
;;prodigy                   ; FIXME managing external services & code builders
rgb                         ; creating color strings
;;taskrunner                ; taskrunner for all your projects
;;terraform                 ; infrastructure as code
;;tmux                      ; an API for interacting with tmux
tree-sitter                 ; syntax and parsing, sitting in a tree...
;;upload                    ; map local to remote projects via ssh/ftp

这两个都是与操作系统有关的补丁,前一个 Mac 用户启用,后一个则都应该启用。

(:if IS-MAC macos)          ; improve compatibility with macOS
(tty +osc)                  ; improve the terminal Emacs experience

编程语言

Doom 提供了几乎所有主流语言的预制模块,大部分都提供了对 LSP 的支持,有些 language server 可以直接在 Emacs 中下载配置,但有些需要另外安装配置。有不少 language server 实现不太好,所以需要自己判断,下面是我启用的一些用得到的语言,大部分都启用了 LSP,可以用 doom doctor 来确认需要另外配置的外部支持软件。另一个值得尝试的选项是 +tree-sitter ,一个强大的静态代码分析插件,官方文档中没有,但实际上存在。另外,无论你主要用什么编程语言, emacs-lisporg 这俩都应该启用,否则还用什么 Emacs 啊!

;;agda                      ; types of types of types of types...
;;beancount                 ; mind the GAAP
;;(cc +lsp)                 ; C > C++ == 1
(clojure +lsp)              ; java with a lisp
;;common-lisp               ; if you've seen one lisp, you've seen them all
;;coq                       ; proofs-as-programs
;;crystal                   ; ruby at the speed of c
;;csharp                    ; unity, .NET, and mono shenanigans
data                        ; config/data formats
;;(dart +flutter)           ; paint ui and not much else
;;dhall
;;elixir                    ; erlang done right
;;elm                       ; care for a cup of TEA?
emacs-lisp                  ; drown in parentheses
;;erlang                    ; an elegant language for a more civilized age
;;ess                       ; emacs speaks statistics
;;factor
;;faust                     ; dsp, but you get to keep your soul
;;fortran                   ; in FORTRAN, GOD is REAL (unless declared INTEGER)
;;fsharp                    ; ML stands for Microsoft's Language
;;fstar                     ; (dependent) types and (monadic) effects and Z3
;;gdscript                  ; the language you waited for
(go +lsp)                   ; the hipster dialect
;;(graphql +lsp)            ; Give queries a REST
(haskell +lsp +tree-sitter) ; a language that's lazier than I am
;;hy                        ; readability of scheme w/ speed of python
;;idris                     ; a language you can depend on
(json +lsp +tree-sitter)    ; At least it ain't XML
(java +lsp)                 ; the poster child for carpal tunnel syndrome
(javascript +lsp)           ; all(hope(abandon(ye(who(enter(here))))))
;;julia                     ; a better, faster MATLAB
;;kotlin                    ; a better, slicker Java(Script)
latex                       ; writing papers in Emacs has never been so fun
;;lean                      ; for folks with too much to prove
;;ledger                    ; be audit you can be
;;lua                       ; one-based indices? one-based indices
(markdown +grip)            ; writing docs for people to ignore
;;nim                       ; python + lisp at the speed of c
;;nix                       ; I hereby declare "nix geht mehr!"
;;ocaml                     ; an objective camel
(org +dragdrop +gnuplot     ; organize your plain life in plain text
     +hugo +journal
     +jupyter +noter
     +pandoc +present
     +pretty +roam2)
;;php                       ; perl's insecure younger brother
plantuml                    ; diagrams for confusing people more
;;purescript                ; javascript, but functional
(python +lsp +pyright)      ; beautiful is better than ugly
;;qt                        ; the 'cutest' gui framework ever
;;racket                    ; a DSL for DSLs
;;raku                      ; the artist formerly known as perl6
rest                        ; Emacs as a REST client
;;rst                       ; ReST in peace
;;(ruby +rails)             ; 1.step {|i| p "Ruby is #{i.even? ? 'love' : 'life'}"}
(rust +lsp +tree-sitter)    ; Fe2O3.unwrap().unwrap().unwrap().unwrap()
;;scala                     ; java, but good
;;(scheme +guile)           ; a fully conniving family of lisps
(sh +tree-sitter)           ; she sells {ba,z,fi}sh shells on the C xor
;;sml
;;solidity                  ; do you need a blockchain? No.
;;swift                     ; who asked for emoji variables?
;;terra                     ; Earth and Moon in alignment for performance.
web                         ; the tubes
;;yaml                      ; JSON, but readable
(zig +lsp)                  ; C, but simpler

输入法

增加一些输入法,在 macOS 系统下基本不需要。

;;bidi                      ; (tfel ot) thgir etirw uoy gnipleh
;;chinese
;;japanese
;;layout                    ; auie,ctsrnm is the superior home row

万能的 Emacs

有人会用 Emacs 来做任何事,包括电子邮件、IRC、RSS 等等,甚至听歌看视频,目前我只用邮件。

(:if (executable-find "mu") (mu4e +org +gmail))
;;notmuch
;;(wanderlust +gmail)

;;calendar
;;emms
;;everywhere                ; *leave* Emacs!? You must be joking
;;irc                       ; how neckbeards socialize
;;(rss +org)                ; emacs as an RSS reader
;;twitter                   ; twitter client https://twitter.com/vnought

基本设置

这里是一些只要 Emacs 启动就需要尽快设置的内容,不需要考虑延迟加载的一些设置,包括一些 Emacs 本身的全局设置,也包括一些需要尽快加载的模块和包的基本设置。

;;; config.el -*- lexical-binding: t; -*-

个人信息

(setq user-full-name "Neo Lee"
      user-mail-address "neo@soulhacker.me")

更好的缺省值

Doom 大部分的缺省配置都是优化且实用的,但还是有少量的需要修改。其中 word-wrap-by-category 这个选项是 Emacs 28 新加的,修复了 CJK 语言文本自动换行的老 bug。

(setq-default
 window-combination-resize t
 x-stretch-cursor t
 yas-triggers-in-field t
 )

(setq
 undo-limit 80000000
 auto-save-default t
 scroll-preserve-screen-position 'always
 scroll-margin 2
 word-wrap-by-category t
 all-the-icons-scale-factor 1.0
 )

(global-subword-mode t)

因为我不用 evil 所以需要给 local leader 一个方便的组合键。

(map! "C-z" nil)
(setq doom-localleader-alt-key "C-z")

如果想显示相对行号,将这个变量值改为 'relative

(setq display-line-numbers-type t)

Org 的根目录需要尽早设置。

(setq org-directory "~/Code/Org/")

缺省配置下 treemacs 只会显示文件的 git 状态,改为 'extended 可以显示文件和目录的状态,另一个选项 'deferred 似乎有问题;这个选项要在 treemacs 加载前设置。

(setq +treemacs-git-mode 'extended)

这个开关可以让 treemacs 将空目录尽可能合并为一项来显示,数值代表同时能合并显示的目录数上限。

(after! treemacs
  :config
  (setq treemacs-collapse-dirs 200))

使用 PlantUML 用本地模式比较靠谱。

(setq plantuml-jar-path (concat (expand-file-name user-emacs-directory) "plantuml/plantuml.jar")
      plantuml-default-exec-mode 'jar)

非图形界面下运行

有时我们无法在图形界面下运行 Emacs,这时就需要下面这些 tweak

(unless (display-graphic-p)
  (map!
   "C-c b" 'treemacs-select-window
   [mouse-4] 'scroll-down-line
   [mouse-5] 'scroll-up-line))

字体和主题

(setq +main-font "Iosevka Fixed")
(setq +unicode-font "Sarasa Fixed SC")

(setq doom-font (font-spec :family +main-font :size 16 :weight 'light)
      doom-big-font (font-spec :family +main-font :size 20 :weight 'light)
      doom-variable-pitch-font (font-spec :family +main-font) ;; inherits :size from doom-font
      doom-serif-font (font-spec :family +main-font :weight 'light)
      doom-unicode-font (font-spec :family +unicode-font)
      )

(setq doom-themes-enable-bold t
      doom-themes-enable-italic t)

随系统切换 light 和 dark mode 主题,利用 macOS 下 emacs-plus 增加的 ns-system-appearance-change-functions 钩子来实现。

;; (setq +light-theme 'doom-one-light
;;       +dark-theme 'doom-vibrant)
;; (setq +light-theme 'modus-operandi
;;       +dark-theme 'modus-vivendi)
;; (setq +light-theme 'doom-acario-light
;;       +dark-theme 'doom-acario-dark)
(setq +light-theme 'doom-flatwhite
      +dark-theme 'doom-city-lights)
;; (setq +light-theme 'doom-opera-light
;;       +dark-theme 'doom-opera)
;; (setq +light-theme 'doom-nord-light
;;       +dark-theme 'doom-nord)

(defun +apply-theme (appearance)
  "Load theme, taking current system APPEARANCE into consideration."
  (mapc #'disable-theme custom-enabled-themes)
  (pcase appearance
    ('light (load-theme +light-theme t))
    ('dark (load-theme +dark-theme t))))

(add-hook! 'ns-system-appearance-change-functions '+apply-theme)

其他关联主题和字体设置, treemacs 适配 nerd-icons 似乎已经合并进 Doom ,不再需要手动设置。

(doom-themes-org-config)

;;(after! treemacs
;;  :config
;;  (doom-themes-treemacs-config)
;;  (treemacs-load-theme "nerd-icons")
;;  )

状态条

Doom 缺省的状态条是 doom-modeline ,可能是几个状态条方案里最漂亮的一个,不过这问题见仁见智,选自己喜欢的就好。目前版本缺省配置就挺好,以前的 hacking 都被我删了。

切换窗口

预制模块 ace-window 加强了 C-x o 的功能,当多于2个窗口时会高亮显示每个窗口左上角的一个字母,按下这个字母就切换到对应窗口,但这个字母的显示非常小,很不醒目,解决方案很简单,定制这个字母显示的样式,把它变大再加上红色底,就醒目了。 ace-window 还有不少别的功能,比如激活之后可以按 x 来删除当前窗口,按 m 来与选择的窗口交换内容等,具体可参考官网说明

(custom-set-faces!
  '(aw-leading-char-face
    :foreground "white" :background "red"
    :weight bold :height 1.5 :box (:line-width 10 :color "red")))

花式符号

预制模块 ligature 及其 +extra 选项会用一些漂亮的文字符号来替代一些固定文本组合,比如用 λ 代替 Lisp 类语言里的 lambdafn 关键字,用 𝒇 代替 function 关键字,用 代替 import 关键字等,没啥用就图个乐,不过在某些语言里这个玩意儿会影响理解和编码,所以对这些语言禁用掉是个明智选择。

(defun +appened-to-negation-list (head tail)
  (if (sequencep head)
      (delete-dups
       (if (eq (car tail) 'not)
           (append head tail)
         (append tail head)))
    tail))

(when (modulep! :ui ligatures)
  (setq +ligatures-extras-in-modes
        (+appened-to-negation-list
         +ligatures-extras-in-modes
         '(not c-mode c++-mode emacs-lisp-mode python-mode scheme-mode racket-mode rust-mode)))
  (setq +ligatures-in-modes
        (+appened-to-negation-list
         +ligatures-in-modes
         '(not emacs-lisp-mode scheme-mode racket-mode))))

语言服务器

Emacs 支持微软开源的 LSP 接口以及各种各样的语言服务器时间还不长,问题也有一些,不过未来可期。 lsp-ui 提供了大量的定制选项。

(after! lsp
  :custom
  (setq lsp-log-io nil
        lsp-print-io nil
        lsp-document-sync-method 'incremental ; none, full, incremental, or nil
        lsp-response-timeout 10
        )
  :hook
  (lsp-mode . lsp-ui-mode))

(after! lsp-ui
  :custom
  (setq
   ; lsp-enable-symbol-highlighting nil
   ; lsp-lens-enable nil
   lsp-headerline-breadcrumb-enable nil
   ; lsp-diagnostics-provider :none
   ; lsp-completion-provider :none
   lsp-ui-doc-enable nil
   lsp-ui-doc-show-with-mouse nil
   lsp-ui-doc-show-with-cursor nil
   lsp-ui-doc-header nil
   lsp-ui-doc-include-signature nil
   lsp-ui-doc-position 'at-point ;; top, bottom, or at-point
   lsp-ui-doc-max-width 120
   lsp-ui-doc-max-height 30
   lsp-ui-doc-use-childframe t
   lsp-ui-doc-use-webkit t
   lsp-ui-sideline-enable nil
   lsp-ui-sideline-show-code-actions t
   lsp-ui-sideline-show-hover nil
   lsp-ui-sideline-ignore-duplicate t
   lsp-ui-sideline-show-symbol t
   lsp-ui-sideline-show-diagnostics t
   lsp-ui-peek-enable t
   lsp-ui-peek-fontify 'on-demand ;; never, on-demand, or always
   lsp-ui-imenu-enable t
   lsp-ui-imenu-kind-position 'top
   )
  :preface
  (defun +toggle-lsp-ui-doc ()
    (interactive)
    (if lsp-ui-doc-mode
      (progn
        (lsp-ui-doc-mode -1)
        (lsp-ui-doc-hide))
      (progn
        (lsp-ui-doc-mode 1)
        (lsp-ui-doc-show))
      ))
  :bind
  (map! :map lsp-mode-map
        ("C-c l r" #'lsp-ui-peek-find-references)
        ("C-c l j" #'lsp-ui-peek-find-definitions)
        ("C-c l i" #'lsp-ui-peek-find-implementation)
        ("C-c l m" #'lsp-ui-imenu)
        ("C-c l s" #'lsp-ui-sideline-mode)
        ("C-c l d" #'+toggle-lsp-ui-doc))
  )

其他一些语言服务器的配置。

(add-hook! 'js-mode-hook (add-to-list 'flycheck-disabled-checkers 'lsp))

(setq lsp-javascript-validate-enable nil
      lsp-javascript-suggest-enabled nil
      lsp-javascript-suggestion-actions-enabled nil
      lsp-zig-zls-executable "~/Code/Repo/zls/zig-out/bin/zls")

拼写检查

无论使用 spell-fu 还是 flyspell 都需要配置个人词汇表的保存位置,我都放在 doom-user-dir 目录下了。另外 flyspell 激活错词修改菜单的缺省操作是鼠标中键(滚轮),MacBook 上没这玩意儿啊,所以我给改成 Command+鼠标主键 了。

(after! spell-fu
  (defun +spell-fu-register-dictionary (lang)
    "Add `LANG` to spell-fu multi-dict, with a personal dictionary."
    ;; Add the dictionary
    (spell-fu-dictionary-add (spell-fu-get-ispell-dictionary lang))
    (let ((personal-dict-file (expand-file-name (format "aspell.%s.pws" lang) doom-user-dir)))
      ;; Create an empty personal dictionary if it doesn't exists
      (unless (file-exists-p personal-dict-file) (write-region "" nil personal-dict-file))
      ;; Add the personal dictionary
      (spell-fu-dictionary-add (spell-fu-get-personal-dictionary (format "%s-personal" lang) personal-dict-file))))
  (add-hook! 'spell-fu-mode-hook
    (+spell-fu-register-dictionary "en")
    ))

(after! flyspell
  :config
  (setq ispell-personal-dictionary (expand-file-name "aspell.pws" doom-user-dir))
  :bind
  (map!
   [s-down-mouse-1] 'flyspell-correct-word)
  )

电子邮件

在配置好 isyncmu 之后(比较 tricky 的部分是使用 GnuPG 来加密密码,可以参考这篇以及这篇),在 Emacs 中使用 mu4e 就很简单了。

(set-email-account! "neo"
  '((mu4e-sent-folder       . "/neo/Sent")
    (mu4e-drafts-folder     . "/neo/Drafts")
    (mu4e-trash-folder      . "/neo/Trash")
    (mu4e-refile-folder     . "/neo/All Mail")
    (smtpmail-smtp-user     . "neo@soulhacker.me")
    (user-mail-address      . "neo@soulhacker.me")
    (mu4e-compose-signature . "--\nNeo Lee a.k.a. soulhacker"))
  t)

(setq send-mail-function 'smtpmail-send-it)
(setq smtpmail-smtp-server "localhost")
(setq smtpmail-smtp-service 1025)
(setq smtpmail-stream-type 'starttls)

(setq org-msg-signature "\n#+begin_signature\n--\nNeo Lee a.k.a. soulhacker\n#+end_signature")

(after! mu4e
  (setq mu4e-headers-auto-update t
        mu4e-update-interval 120
        mu4e-view-show-images t
        mu4e-use-fancy-chars t)
  )

补充软件包

Doom 的缺省配置和预制模块能覆盖我绝大部分需求,所以需要我另外添加的软件包很少,这些软件包需要写在 packages.el 里。

;;; packages.el -*- no-byte-compile: t; -*-

窗口移动

用快捷键快速在几个窗口间交换 buffer,比如同时打开 .c.h 文件,先在左边窗口编辑 .h 文件,然后把右边窗口的 .c 文件换过来进行编辑,使用这个包用一个组合键就能做到。其实预制模块里的 ace-window 也可以做类似的事情,但 buffer-mover 一个按键搞定更快。这里我用 :recipe 子句来禁用这个包的编译,因为这个包有个 bug,编译后会报错。

(package! buffer-move :recipe (:build (:not compile)))

安装和启用之后,在 config.el 里配置自己习惯的按键组合(我用 Control+Command+方向键 ):

(map!
 [C-s-up]        #'buf-move-up
 [C-s-down]      #'buf-move-down
 [C-s-left]      #'buf-move-left
 [C-s-right]     #'buf-move-right
 )

锁定可靠版本

缺省的 Doom Emacs 有着过于激进的版本跟进策略,有些包的 master 分支并不可信,一旦出现问题我们需要锁定在可靠的版本上。

org-mode

最新的 org-mode 9.7 开发分支有不少问题,而 Doom Emacs 缺省会跟着 org-modemaster 分支,经常会破坏一些本来正常工作的软件包,所以我们目前需要锁在 9.6 最新稳定版本。

(package! org :pin "806abc5a2bbcb5f884467a0145547221ba09eb59")

另外一些与 org-mode 相关的软件包也需要锁在相对应的较早版本上。

(package! jupyter :pin "455166712e606c9c6a8de763ea0a77548cadcef2")
(package! ox-hugo :pin "3c8c0bf28cf02128ceff9a972db3ebad01083953")

知识网络

org-roam-ui 提供强大的知识网络笔记系统 org-roam 的网络可视化,并可与 org-roam 紧密整合。注意 org-roam-ui 只支持 org-roam v2 ,如果使用 org-roam v1 的话请用 org-roam-server

(package! org-roam-ui)

公式预览

xenopsorg-fragtog 都是用来在 org-mode 里实时渲染显示 \(LaTeX\) 公式的,它们都会实时将公式渲染为图片(前者用 SVG 格式,后者用 PNG 格式)并缓存,然后显示出来,区别在于 org-fragtog 是同步模式,渲染时会卡住 Emacs,等它好了你才能继续操作,而 xenops 使用 emacs-aio 实现异步渲染,它干活的时候你也可以继续干活,它渲染好就会自动显示出来。

(package! xenops)

编程语言 - Clojure

clj-refactor 这个包需要和后端 Clojure 的 refactor-nrepl 包版本同步,且它们更新都挺频繁,所以最好解放版本限制。

(unpin! clj-refactor)

编程语言 - Python

Doom 其实有个大一统的源代码格式美化模块( :editor format ),但我还没时间深入研究,而且这个模块和 LSP 是彼此独立的,不知道有什么坑,所以没开。针对比较有这个需求的 Python 语言单独加了 black 这个懒汉的选择,作为 LSP 方案的保底。

(package! python-black)

小玩具

keycast 可以跟踪并显示所有按键操作,在录制与 Emacs 有关的 screencast 时有用,其实我用的很少,ScreenFlow 提供更漂亮的解决方案,但在 Linux 下是个不错的方案。

(package! keycast)

这个包允许你在 Emacs 里查询、下载和查看 xkcd 漫画,嘿嘿嘿!

(package! xkcd)

Org Mode

org-mode 是 Emacs 的招牌应用,一个全副武装的 org-mode 可以在一致的环境下完成日程计划、笔记、博客、论文等各种写作任务,不仅可以写出文本、结构化列表、表格,还可以插入图片、 \(LaTeX\) 公式、可运行的程序代码、可追踪的任务,还可以借助 org-roam 实现笔记的知识网络化。这些功能是以 org-mode 为中心的一组软件包配合实现的,所以配置也相对多一点。

基本配置

设置 org-agenda 使用的 Org 文档的位置,可以指定目录和文件名 pattern 把所有 .org 文件放进去:

(setq org-agenda-files (directory-files-recursively org-directory "\\.org$"))

但通常还是指定几个分类任务文件就好了。

(after! org
  (setq org-agenda-files
        (list (expand-file-name "Inbox.org" org-directory)
              (expand-file-name "Private.org" org-directory)
              (expand-file-name "Work.org" org-directory)
              (expand-file-name "Projects.org" org-directory)
              (expand-file-name "Notes.org" org-directory)
              )))

另一个和 org-agenda 有关的场景,就是用 org-roam 写东西很快就会有非常多的文件,有些笔记一时写不完可以加上 TODO 标签,然后就需要一个又快又方便的手段来整理这些 TODO 列表,对这个问题 Emacs 社区有位 Boris Buliga(a.k.a d12frosted,这位还是 homebrew-emacs-plusflyspell-correct 的维护者)给出了一个漂亮的解决方案,简单说就是检测到 TODO 时自动给文件打一个 project 标签(如果检测到当前文件所有 TODO 都没有了就自动移除标签),然后直接通过 org-roam 的数据库查询动态将这些文件添进 org-agenda 文件列表。从他分享的 gist 克隆代码到本地,然后加载就可以了。

(after! org-agenda
  (load! "lisp/vulpea-agenda"))

原来的版本有个问题,就是我们上面给变量 org-agenda-files 指定的文件会被移除,所以我做了一点 patch 之后才合用

Org 文档导出 \(LaTeX\) 文档时需要做一些配置,一是引入 ctex 包,否则中文处理会有问题;二是配置一下带链接文本的格式,不然会是带红色框的文本,非常恐怖;另外是一些 \(LaTeX\) 的常规设置。

(after! org
  (setq org-latex-default-packages-alist
        (remove '("AUTO" "inputenc" t) org-latex-default-packages-alist))
  (setq org-latex-pdf-process '("xelatex -interaction nonstopmode %f"
                                "xelatex -interaction nonstopmode %f"))
  (setq org-latex-packages-alist
        '(("" "ctex" t)))
  (setq org-latex-hyperref-template
        "\\hypersetup{linktoc=all,colorlinks=true,urlcolor=blue,linkcolor=blue}")
  )

缺省的 \(LaTeX\) 模板的目录之后直接会接上正文,但一般的排版习惯会希望正文另起一页,这个简单定制一下就可以了:

(setq org-latex-toc-command "\\tableofcontents \\clearpage\n\n")

在编辑 Org 文档时,经常点击链接跳转到另一篇笔记,这时候快速返回可以设置为快捷键,我用的是 Control-Command-b

(after! org
  (map! :map org-mode-map
        "C-s-b" #'org-mark-ring-goto
        ))

维护很多 Org 笔记时,记下笔记最后编辑时间会很有用,这个事情很适合交给 Emacs 自己做,可以利用 Emacs 的 Time Stamp 功能实现。

(after! org
  (add-hook 'org-mode-hook
            (lambda ()
              (setq-local time-stamp-active t
                          time-stamp-line-limit 18
                          time-stamp-start "^#\\+last_modified: [ \t]*"
                          time-stamp-end "$"
                          time-stamp-format "\[%Y-%m-%d %a %H:%M:%S\]")
              (add-hook 'before-save-hook 'time-stamp nil 'local))))

需要这个功能的 Org 笔记在 header 里加入下面一行即可(在笔记的前18行都可以)。

#+last_modified: [ ]

运行代码块

借助 org-babel 来实现可运行的代码块,可以在代码块中应用对应的 major mode ,提供良好的语法高亮及编辑功能。

(after! org-src
  (add-to-list 'org-src-lang-modes '("plantuml" . plantuml)))

另外我还会用 Jupyter 来运行不同语言引擎。注意第一段里 :kernel 参数,需要设为系统里存在的某个 Jupyter kernel 的名字,且系统环境里应该装有 Jupyter(即系统 PATH 中能找到 jupyter 程序);使用中如果需要 pip 安装 Python 软件包,也是安装在这个 kernel 环境下。

(setq org-babel-default-header-args:jupyter-python '((:async . "no")
                                                     (:session . "jp")
                                                     (:kernel . "wop")))

(after! org
  (require 'org-tempo)
  (org-babel-do-load-languages 'org-babel-load-languages
                               '((emacs-lisp . t)
                                 (python . t)
                                 (shell . t)
                                 (latex . t)
                                 (sql . t)
                                 (sqlite . t)
                                 (calc . t)
                                 (jupyter . t)))
  )

(after! org-src
  (dolist (lang '(python))
    (cl-pushnew (cons (format "jupyter-%s" lang) lang)
                org-src-lang-modes :key #'car))
  )

知识网络

org-roam

其实我刚开始用 org-roam ,所以只有最简单的配置,一些复杂的模板之类的都还没开始用。这里最重要的配置是 Roam 笔记的根目录,另外我稍微修改了下缺省的 capture template ,在元数据里加了 tag

(use-package! org-roam
  :custom
  (org-roam-directory (file-truename "~/Code/Org/roam/"))
  (org-roam-completion-everywhere t)
  :bind (("C-c n l" . org-roam-buffer-toggle)
         ("C-c n f" . org-roam-node-find)
         ("C-c n g" . org-roam-graph)
         ("C-c n i" . org-roam-node-insert)
         ("C-c n c" . org-roam-capture)
         ("C-c n j" . org-roam-dailies-capture-today))
  :config
  (setq org-roam-capture-templates
        `(("d" "default" plain "%?"
           :if-new (file+head "%<%Y%m%d%H%M%S>-${slug}.org"
                              "#+title: ${title}\n#+filetags:\n\n")
           :unnarrowed t)))
  (org-roam-db-autosync-mode)
  (require 'org-roam-protocol))

org-roam-ui

这个知识网络的可视化会显示在浏览器中,通过 websocket 与 Emacs 通信。

(after! org-roam
  (use-package! websocket)
  (use-package! org-roam-ui
    :config
    (setq org-roam-ui-sync-theme t
          org-roam-ui-follow t
          org-roam-ui-update-on-save t
          org-roam-ui-open-on-start t))
  )

实时公式预览

xenops 基本配置,注意公式显示的大小可用变量 xenops-math-image-scale-factor 微调。

(when (display-graphic-p)
  (use-package! xenops
    :hook (latex-mode . xenops-mode)
    :hook (LaTeX-mode . xenops-mode)
    :hook (org-mode . xenops-mode)
    :defer t
    :config
    (map! :map xenops-mode-map
          :n "RET" #'xenops-dwim)
    (setq xenops-cache-directory (concat doom-cache-dir "xenops/")
          xenops-reveal-on-entry t
          xenops-math-latex-process 'dvisvgm
          xenops-math-image-scale-factor 1.5
          )))

其他配置

Smartparens

因为我用 lipsy 所以其实 smaertparens 用的不多,不过作为保底,还是配了一下常用按键组合。

(map!
 (:after smartparens
  (:map smartparens-mode-map
   "C-M-f"       #'sp-forward-sexp
   "C-M-b"       #'sp-backward-sexp
   "C-M-u"       #'sp-backward-up-sexp
   "C-M-d"       #'sp-down-sexp
   "C-M-p"       #'sp-backward-down-sexp
   "C-M-n"       #'sp-up-sexp
   "C-)"         #'sp-forward-slurp-sexp
   "C-}"         #'sp-forward-barf-sexp
   "C-("         #'sp-backward-slurp-sexp
   "C-{"         #'sp-backward-barf-sexp
   "C-M-s"       #'sp-splice-sexp
   ))
 )

Python

几个小工具方便一些常用操作,包括用 black 格式化源代码。

(after! python
  :preface
  (defun +python-make-fstring ()
    "Change string to fstring"
    (interactive)
    (when (nth 3 (syntax-ppss))
      (let ((p (point)))
        (goto-char (nth 8 (syntax-ppss)))
        (insert "f")
        (goto-char p)
        (forward-char))))
  (defun +python-format-buffer ()
    "Format python buffer with black"
    (interactive)
    (python-black-buffer))
  :bind
  (map! :map python-mode-map
        "C-c x s" #'+python-make-fstring
        "C-c x f" #'+python-format-buffer))

Clojure

CIDER 是 Clojure 的 REPL ,缺省会作为一个 popup 显示在窗口下方,但一般来说习惯于左右分屏,所以需要修改;后面试 CIDER 的几个设置,一个可以在切换 namespace 的时候自动加载对应代码,另一个指定 =cider-jack-in 时使用 deps.edn 中哪个配置 alias

(after! cider
  (set-popup-rules!
    '(("^\\*cider-repl"
       :side right
       :width 100
       :quit nil
       :ttl nil)))
  (setq cider-repl-require-ns-on-set t)
  (setq cider-clojure-cli-aliases ":cider")
  )

Prolog

这个语言比较小众,所以需要设置一下缺省的文件后缀,自动对应 major-mode ,一般来说后缀用 plpro 都可以,前者 Perl 语言会用,所以我们用后一个:

(setq auto-mode-alist
      (append '(("\\.pro" . prolog-mode))
              auto-mode-alist))

Web

web-modeauto indentation 经常有奇怪的行为,还是关了好。

(setq web-mode-enable-auto-indentation nil)

keycast

doom-modeline 需要做一些 tweak 才能用。

(use-package! keycast
  :commands keycast-mode
  :config
  (define-minor-mode keycast-mode
    "Show current command and its key binding in the mode line."
    :global t
    (if keycast-mode
        (progn
          (add-hook 'pre-command-hook 'keycast--update t)
          (add-to-list 'global-mode-string '("" keycast-mode-line " ")))
      (remove-hook 'pre-command-hook 'keycast--update)
      (setq global-mode-string (remove '("" keycast-mode-line " ") global-mode-string))))
  (custom-set-faces!
    '(keycast-command :inherit doom-modeline-debug :height 1.0)
    '(keycast-key :inherit custom-modified :height 1.1 :weight bold)))

xkcd

干活累了看看漫画,不亦乐乎!

(use-package! xkcd
  :commands (xkcd-get xkcd)
  :config
  (setq xkcd-cache-dir (expand-file-name "xkcd/" doom-cache-dir)
        xkcd-cache-latest (expand-file-name "xkcd/latest" doom-cache-dir)))

  1. Literate Config:这个很酷的想法来源于 Donald Knuth 老爷子40年前提出的 Literate Programming ,就是把文字和程序写在同一文档里,用两个程序来处理这个文档,一个叫 tangle ,可以把里面所有代码提取出来组成一个可以运行的程序,另一个叫 weave ,可以将文档整体导出成各种可读格式供人阅读。关于这个优雅想法的更深入讨论,可以读读这本书。 ↩︎