我的 Emacs 配置

我用 Emacs 作为自己的开发工具已经有十多年,但是配置其实不复杂,大多是自己从网络上东拼西凑得来。这些配置都写在一个 org 文件里,通过 org babel 源码块得到 el 配置文件。这样,在 ~/.emacs.d/init.el 里只要写 (org-babel-load-file "/path/to/this/file") 即可。一些不再用的,或仅做示例的配置通过 :tangle no 禁掉即可。

配置 use-package

(require 'package)
(setq package-archives '(("gnu"    . "http://mirrors.tuna.tsinghua.edu.cn/elpa/gnu/")
                         ("nongnu" . "http://mirrors.tuna.tsinghua.edu.cn/elpa/nongnu/")
                         ("melpa"  . "http://mirrors.tuna.tsinghua.edu.cn/elpa/melpa/")))
(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t)
(package-initialize)
(when (not package-archive-contents)
  (package-refresh-contents))
(unless (package-installed-p 'use-package)
  (package-install 'use-package))

(require 'use-package)
(setq use-package-always-ensure t)
(setq use-package-always-defer t)
(setq use-package-expand-minimally t)
(use-package use-package-ensure-system-package)

基本配置

(use-package benchmark-init
  :config
  :hook
  (after-init . benchmark-init/deactivate))

(use-package emacs
  :init
  (setq gc-cons-threshold 10000000)
  :hook
  (after-init . (lambda ()
                  (message "Loaded in %s with %d garbage collections."
                           (format "%.2f seconds" (float-time (time-subtract after-init-time before-init-time)))
                           gcs-done)
                  (setq gc-cons-threshold 1000000)
                  (message "gc-cons-threshold restored to %S"
                           gc-cons-threshold))))
(use-package emacs
  ;; :custom
  ;; ;; fullscreen emacs window get right and bottom white margins in spectrwm
  ;; (initial-frame-alist (quote ((fullscreen . fullscreen))))
  :config
  (setq user-mail-address "HexingB@qq.com")
  (setq user-full-name "Bao Hexing")
  (setq inhibit-startup-message t)
  (setq initial-scratch-message "")
  (menu-bar-mode -1)

  (if (display-graphic-p)
      (progn
          (set-scroll-bar-mode nil)
        ))

  (setq-default cursor-type 'box)
  (tool-bar-mode -1)
  (defalias 'yes-or-no-p 'y-or-n-p)
  (setq show-paren-delay 0)
  (setq show-paren-style 'expression) ;; or 'parenthesis
  (global-hl-line-mode nil)
  (setq-default make-backup-files nil)
  (setq ring-bell-function 'ignore)
  (setq-default indent-tabs-mode nil)
  (setq-default tab-width 4)
  (setq custom-file (expand-file-name "custom.el" user-emacs-directory))
  (if (file-readable-p custom-file)
      (load custom-file))
  (prefer-coding-system 'utf-8)
  (setq default-buffer-file-coding-system 'utf-8)
  (setq locale-coding-system 'utf-8)
  (set-file-name-coding-system 'utf-8)
  (set-clipboard-coding-system 'utf-8)
  (set-buffer-file-coding-system 'utf-8)
  (set-terminal-coding-system 'utf-8)
  (set-keyboard-coding-system 'utf-8)
  (set-language-environment 'utf-8)
  (set-default-coding-systems 'utf-8)
  (setq system-time-locale "C")
  (unless (eq system-type 'windows-nt)
    (set-selection-coding-system 'utf-8))
  (setenv "LANG" "en_US.UTF-8")
  (global-visual-line-mode)
  (column-number-mode)
  (global-so-long-mode)
  (global-auto-revert-mode)
  (electric-pair-mode)
  (setq desktop-restore-eager 2
    desktop-dirname         "~/.emacs.d/"
    desktop-base-file-name      "emacs.desktop"
    desktop-base-lock-name      "lock"
    desktop-path                (list desktop-dirname)
    desktop-save                t
    desktop-files-not-to-save   "^$" ;reload tramp paths
    desktop-load-locked-desktop nil
    desktop-auto-save-timeout   30)
  (desktop-save-mode)
  (auto-fill-mode)
  (setq large-file-warning-threshold 5000000)
  (which-function-mode))

工具类包

(use-package exec-path-from-shell
  :ensure t
  ;; :if (memq window-system '(mac ns))
  ;; :custom (exec-path-from-shell-check-startup-files nil)
  :config (exec-path-from-shell-initialize)
  ;; :hook
  ;; (after-init . (exec-path-from-shell-initialize))
  )

(use-package mwim
  :bind (("C-a" . mwim-beginning-of-code-or-line)
         ("C-e" . mwim-end-of-code-or-line)))

(use-package keyfreq
    :hook
    (after-init . (keyfreq-mode keyfreq-autosave-mode)))

(use-package recentf
  :defer 3
  :custom
  (recentf-auto-cleanup 432000)
  (recentf-max-saved-items 128)
  (recentf-save-file (concat user-emacs-directory "/recentf"))
  :hook (after-init . recentf-mode)
  )

(use-package crux
  :bind (("C-k" . crux-smart-kill-line)
         ("C-c f" . crux-recentf-find-file)
         ("C-c r" . crux-rename-file-and-buffer)))
(use-package vlf
  :config
  (require 'vlf-setup))
(use-package projectile
  :defer 1
  :hook
  (after-init . projectile-global-mode)
  :custom
  (projectile-enable-caching t)
  (projectile-indexing-method 'hybrid projectile-enable-caching t)
  :config
  (add-to-list 'projectile-globally-ignored-directories ".cache")
  :bind
  (:map projectile-mode-map
   ("C-x p p" . projectile-switch-project)
   ("C-x p k" . projectile-kill-buffers)
   ("C-x p f" . projectile-find-file)
   ("C-x p r" . projectile-replace-regexp)
   ("C-x p C-b" . projectile-ibuffer)
   ("C-x p C-r" . projectile-remove-known-project)
   ("C-x p o" . projectile-find-other-file)
   ("C-x p v" . projectile-vc)
   ("C-x p c" . project-compile))
  )

疑问:如何在 use-package :map 中设置 projectile 的键映射,但是不带 "C-x p" 而是使用 projectile-keymap-prefix?但是 projectile-keymap-prefix 是 nil 啊,那为什么我可以按 C-x p 时弹出 projectile 的操作界面?

.dir-locals.el 中, 可以给这些变量设置合适的值来执行编译和测试:

  • projectile-project-compilation-cmd 设置编译命令
  • projectile-project-test-cmd 设置测试命令
  • projectile-project-run-cmd 设置运行命令
  • projectile-project-compilation-dir 设置执行编译的目录

如下例:

((nil . ((projectile-project-compilation-dir . ".")
         (projectile-project-compilation-cmd . "make"))))

文档里写可以通过 projectile-edit-dir-locals 命令编写 .dir-locals.el ,但我不知道写完后如何终止退出。所有我用 add-dir-local-variable 命令写 compile-command 变量。

(use-package swiper
  :bind (("C-s" .
          (lambda () (interactive)
            (swiper (format "%s" (let ((sym (thing-at-point 'symbol)))
                                   (if sym sym ""))))))))

(use-package counsel
  :config
  ;; (setq counsel-ag-base-command "ag --nocolor --group %s")
  (setq counsel-ag-command "rg --vimgrep %s")
  :bind (("M-x" . counsel-M-x) ;; Gives M-x command counsel features
         ("M-p" . counsel-yank-pop) ;; pop all yank, or "pop paste" for remember
         ("C-x C-f" . counsel-find-file)
         ("M-g i" . counsel-imenu) ;; I don't know why imenu command don't jump to correct position
         ("C-c s" .
          (lambda () (interactive)
            (counsel-rg (format "%s" (let ((sym (thing-at-point 'symbol)))
                                       (if sym sym ""))))))))

(use-package ivy
  :hook
  (after-init . ivy-mode)
  :config
  ;; changes the format of the number of results
  (setq ivy-count-format "(%d/%d) ")
  (setq ivy-use-virtual-buffers t)
  (setq ivy-use-selectable-prompt t)
  (setq enable-recursive-minibuffers t)

  ;; number of result lines to display
  (setq ivy-height 36)
  ;; no regexp by default
  (setq ivy-initial-inputs-alist nil)
  ;; configure regexp engine.
  (setq ivy-re-builders-alist
        ;; allow input not in order
        '((t . ivy--regex-ignore-order))))

(use-package ivy-rich
  :custom
  (ivy-virtual-abbreviate 'full)
  (ivy-rich-switch-buffer-align-virtual-buffer nil)
  (ivy-rich-path-style 'full)
  :config
  (setcdr (assq t ivy-format-functions-alist) #'ivy-format-function-line)
  :hook (after-init . ivy-rich-mode)
  )
(use-package hi-lock
  :config
  (setq hi-lock-face-defaults '("hi-yellow" "hi-pink" "hi-green" "hi-salmon" "hi-aquamarine" "hi-red-b" "hi-green-b" "hi-black-hb")))
(use-package emacs
  :config
  (load-theme 'leuven-dark)
  )

(use-package smart-mode-line
  :defer 2
  :init
  (setq sml/theme 'respectful)
  (setq sml/no-confirm-load-theme t)
  (setq sml/mode-width 0)
  (setq sml/name-width '(16 . 48))
  :config
  (sml/setup)
  :custom
  (rm-blacklist
        (format "^ \\(%s\\)$"
                (mapconcat #'identity
                           '("Projectile.*" "company.*" "AcePY" "ws" "hs" "Wrap" "Abbrev" "ElDoc" "Ind" "Flymake.*" "ELDOC-BOX"
                             "Undo-Tree" "counsel" "ivy" "yas" "WK" "GG" "Hi" "Apheleia")
                           "\\|")))
  (sml/replacer-regexp-list
   '(("^~/\\.emacs\\.d/"  ":EMACS:")
     ("^~/work/*"         "[work]")
     )))

如下配置界面。但我最后还是直接使用了自带主题。

(use-package emacs
  :after smart-mode-line
  :config
  (set-face-attribute 'mode-line nil
                      :underline "Green"
                      :overline "Green"
                      :background (face-background 'default)
                      :foreground (face-foreground 'default)
                      )
  (set-face-attribute 'mode-line-inactive nil
                      :underline t
                      :overline t
                      :background (face-background 'default)
                      :foreground (face-foreground 'default)
                      )
  (set-background-color "#1A2421") ;; Dark Jungle Green(#1a2421) or Deep Jungle Green(#004b49)
  (set-foreground-color "#F2F0EB") ;; Snow White(#F2F0EB) or Angel Blue(#96AED0)
  )

左右两侧会有白边,如下消除:

(use-package emacs
  :if (display-graphic-p)
  :config
  (set-fringe-mode '(0 . 0))
  )

简单的字体配置:

(use-package emacs
  :if (display-graphic-p)
  :config
  (when (eq system-type 'darwin)
    (setq fonts '("Noto Sans Mono" "LXGW WenKai Mono"))
    (set-fontset-font t 'unicode "Apple Color Emoji" nil 'prepend)
    (set-face-attribute 'default nil :font
            (format "%s:pixelsize=%d" (car fonts) 18)))

  (when (eq system-type 'windows-nt)
    (setq fonts '("Consolas" "微软雅黑"))
    (set-fontset-font t 'unicode "Segoe UI Emoji" nil 'prepend)
    (set-face-attribute 'default nil :font
            (format "%s:pixelsize=%d" (car fonts) 24)))

  ;; download ttf fonts from https://github.com/lxgw/LxgwWenKai release page
  ;; copy these ttf files into /usr/share/fonts/lxgw/
  ;; run ```fc-cache -fv```
  ;; restart emacs to make it take effect
  (when (eq system-type 'gnu/linux)
    (setq fonts '("Fira Code" "LXGW WenKai Mono"))
    (set-fontset-font t 'unicode "Noto Color Emoji" nil 'prepend)
    (set-face-attribute 'default nil :font
            (format "%s:pixelsize=%d" (car fonts) 24)))

  (if (display-graphic-p)
  (dolist (charset '(kana han symbol cjk-misc bopomofo))
    (set-fontset-font (frame-parameter nil 'font) charset
              (font-spec :family (car (cdr fonts))))))
  )

简单的实用命令:

(use-package emacs
  :config
  (defun my-insert-date ()
    (interactive)
    (insert (format-time-string "%Y-%m-%d %H:%M" (current-time))))
  (global-set-key (kbd "C-c m d") 'my-insert-date)
  (defun my-insert-calendar()
    (interactive)
    (insert (format-time-string "%Y-%m-%d" (current-time))))
  (global-set-key (kbd "C-c m c") 'my-insert-calendar)
  (global-set-key (kbd "M-SPC") 'set-mark-command)
  (global-set-key (kbd "C-x k") 'kill-this-buffer)
  (global-set-key (kbd "C-c l") #'dictionary-lookup-definition)
  )

我用了一段时间 use-package-chords ,并设置 ",," 为快捷键执行 =ace-pinyin-jump-char-2=,但是发现偶尔失效,只好放弃。

(use-package use-package-chords
  :demand t
  :hook
  (ater-init . key-chord-mode)
  :config
  (setq key-chord-two-keys-delay 0.2)
  (setq key-chord-one-key-delay 0.2) ; default 0.2
  )

如果使用 GUI,则用 C-, 也行。但是如果是在终端下使用,则 C-, 被终端截获,所以改用 C-x ,

(use-package ace-pinyin
  :hook (after-init . ace-pinyin-global-mode)
  :bind
  ("C-,"   . ace-pinyin-jump-char-2)
  ("C-x ,"   . ace-pinyin-jump-char-2)) ;; or use ace-pinyin-jump-char
(use-package display-line-numbers
  :hook ((prog-mode yaml-mode) . display-line-numbers-mode)
  :custom (display-line-numbers-width-start t))
(use-package which-key
  :config
  (which-key-setup-minibuffer)
  (which-key-setup-side-window-bottom)
  :custom
  (which-key-enable-extended-define-key t)
  (which-key-idle-delay 0.5)
  :hook
  (prog-mode . which-key-mode))
(use-package persistent-scratch
  :bind (:map persistent-scratch-mode-map
              ([remap kill-buffer] . (lambda (&rest _)
                                       (interactive)
                                       (user-error "Scratch buffer cannot be killed")))
              ([remap revert-buffer] . persistent-scratch-restore)
              ([remap revert-this-buffer] . persistent-scratch-restore))
  :hook ((after-init . persistent-scratch-autosave-mode)
         (lisp-interaction-mode . persistent-scratch-mode))
  :init (setq persistent-scratch-backup-file-name-format "%Y-%m-%d"
              persistent-scratch-backup-directory
              (expand-file-name "persistent-scratch" user-emacs-directory)))

切换窗口布局,有如下一些命令:

  1. transpose-frame
  2. flip-frame
  3. flop-frame
  4. rotate-frame
(use-package transpose-frame)

我发现这个 autoinsert 和 yasnippet 有点重,所有不常用。

(use-package autoinsert
  :init
  (setq auto-insert-query t)
  (setq auto-insert-directory (locate-user-emacs-file "templates"))
  :hook
  (find-file-hook . auto-insert)
  (after-init . auto-insert-mode)
  :config
  (defun my/autoinsert-yas-expand()
    "Replace text in yasnippet template."
    (yas-expand-snippet (buffer-string) (point-min) (point-max)))
  (define-auto-insert "\\.cc?$" ["default-cc.cc" my/autoinsert-yas-expand])
  (define-auto-insert "\\.hh?$" ["default-hh.hh" my/autoinsert-yas-expand])
  (define-auto-insert "\\.cpp?$" ["default-cpp.cpp" my/autoinsert-yas-expand])
  (define-auto-insert "\\.h?$" ["default-h.h" my/autoinsert-yas-expand])
  (define-auto-insert "\\.hpp?$" ["default-hpp.hpp" my/autoinsert-yas-expand]))

还是直接用 yasnippet 就好。下面是 c-lang-common/head 的例子:

# -*- mode: snippet -*-
# name: head
# key: head
# --
/**
 * $1
 *
 * Author: `user-full-name` <`user-mail-address`>
 * Copyright © `(format-time-string "%Y")`, `user-full-name`, all rights reserved.
 * Created: `(format-time-string "%e %B %Y")`
 */

$0

这是 org-mode/head 的例子:

# -*- mode: snippet -*-
# name: head
# key: head
# --
#+options: ':t *:t -:t ::t <:t H:3 \n:nil ^:{} arch:headline author:t
#+options: broken-links:nil c:nil creator:nil d:(not "LOGBOOK")
#+options: date:t e:t email:nil f:t inline:t num:nil p:nil pri:nil
#+options: prop:nil stat:t tags:t tasks:t tex:t timestamp:t title:t
#+options: toc:3 todo:t |:t
#+title: $1
#+date: <`(format-time-string "%Y-%m-%d")`>
#+author: `user-full-name`
#+email: `user-mail-address`
#+language: $2
#+select_tags: export
#+exclude_tags: noexport
#+creator: Emacs `emacs-major-version`.`emacs-minor-version` (Org mode `org-version`)
#+cite_export:

$0
(use-package markdown-mode
  :defer 10
  :mode ("README\\.md\\'" . gfm-mode)
  :init
  (setq markdown-command "pandoc --mathjax -t html5 --file-scope=false")
  (setq markdown-display-remote-images t)
  (setq markdown-enable-math t))

编程

(use-package json-mode)
(use-package yaml-mode)
(use-package typescript-mode
  :pin nongnu
  :mode ("\\.tsx?\\'" . typescript-mode)
  :config
  (setq typescript-indent-level 2))
(use-package realgud
  :pin gnu
  :commands (realgud:gdb realgud:pdb))
(use-package realgud-lldb
  :pin gnu
  :commands (realgud--lldb lldb))
(use-package company
  :config
  (setq company-idle-delay 0)
  (setq company-minimum-prefix-length 1)
  (setq company-tooltip-align-annotations t)
  (setq company-selection-wrap-around t)
  (setq company-show-numbers t)
  (setq company-insertion-on-trigger nil)
  ;; uncomment this if you prefer tab instead of enter key
  ;; (company-tng-configure-default)
  :hook
  (after-init . global-company-mode)
  )
(use-package company-quickhelp
    :defer t
    :init (with-eval-after-load 'company
            (company-quickhelp-mode)))
(use-package yasnippet
  :defer 2
  :config
  (setq require-final-newline nil)
  ;; (setq yas/root-directory "~/.emacs.d/snippets")
  ;; (yas/load-directory yas/root-directory)
  :hook
  ((prog-mode . yas-minor-mode)
   (org-mode . yas-minor-mode))
  :custom
  (yas-prompt-functions '(yas-completing-prompt))
  )
(use-package yasnippet-snippets :after yasnippet)
(use-package apheleia
  :defer 4
  :hook
  (after-init . apheleia-global-mode))

有时候,不想要自动格式化,可以添加变量 apheleia-inhibit.dir-locals.el 中,用 add-dir-locale-variable 命令。

(use-package quickrun
  :defer 10
  :config
  (quickrun-add-command "c++/c1z"
    '((:command . "g++")
      (:exec    . ("%c -std=c++20 %o -o %e %s"
                   "%e %a"))
      (:remove  . ("%e")))
    :default "c++")
  :bind (("C-c q r" . quickrun)))

似乎 git 已经一统天下,magit 必不可少。

(use-package magit
  :commands (magit-status magit-blame magit-status-fullscreen magit-dispatch magit-checkout)
  :init
  (use-package dash)
  :custom
  ;; display magit-status and diff buffer fullscreen
  (magit-display-buffer-function #'magit-display-buffer-fullcolumn-most-v1)
  :config
  (add-hook 'magit-mode-hook (lambda ()
                               (setq whitespace-mode nil)
                               ;; force magit-status buffer vertically opened
                               ;; (setq split-width-threshold 0)
                               ;; (setq split-height-threshold nil)
                               )))
(use-package eldoc
  :defer 10
  :hook (after-init . global-eldoc-mode)
  :config
  (setq eldoc-echo-area-use-multiline-p 3
        eldoc-echo-area-display-truncation-message nil))

(use-package eldoc-box
  :custom
  (eldoc-box-max-pixel-width 720)
  (eldoc-box-max-pixel-height 900)
  :hook (prog-mode . eldoc-box-hover-mode))
(use-package flymake
  :defer 15
  :hook (prog-mode . flymake-mode)
  :bind (("C-h ." . display-local-help)
         :map flymake-mode-map
         ("C-c e n" . flymake-goto-next-error)
         ("C-c e p" . flymake-goto-prev-error)
         ("C-c e b" . flymake-show-diagnostics-buffer)
))
(use-package emmet-mode
  :commands (emmet-mode emmet-expand-line yas/insert-snippet yas-insert-snippet company-complete)
  :hook ((sgml-mode css-mode web-mode) . emmet-mode)
  :config
  (setq emmet-move-cursor-between-quotes t)
  (setq emmet-indent-after-insert nil)
  (unbind-key "C-M-<left>" emmet-mode-keymap)
  (unbind-key "C-M-<right>" emmet-mode-keymap)
  :bind
  ("C-j" . emmet-expand-line)
  ((:map emmet-mode-keymap
         ("C-c [" . emmet-prev-edit-point)
         ("C-c ]" . emmet-next-edit-point)))
  )

下面这个似乎不起作用。看 这里

(use-package cc-mode
  :config
  (defun my-c-mode-common-hook()
    ;; (c-toggle-auto-hungry-state 1) ;; hungry-delete and auto-newline
    (setq c-macro-shrink-window-flag t)
    (setq c-macro-preprocessor "cpp")
    (setq c-macro-cppflags " ")
    (setq c-macro-prompt-flag t)
    (setq abbrev-mode t)
    (setq-local tab-width 4)
    (setq-local c-basic-offset 4)
    )
  :hook (c-mode-hook . my-c-mode-common-hook))
(use-package cc-mode
  :custom
  (c-basic-offset 4))
(use-package python
  :defer 3
  :custom
  (python-indent-guess-indent-offset-verbose nil)
  (python-indent-offset 4)
  (python-shell-interpreter "python3")
  (python-shell-interpreter-args "-i")
  (python-shell-prompt-detect-failure-warning nil)
  (python-shell-completion-native-enable nil)
  )

conda、poetry 和 pyvenv 这三个包,在 emacs 中的配置和使用让我很是迷惑了一段时间。这三者各有自己的功能。管理虚拟环境是他们共有的功能。conda 更重一些,可以创建指定版本的 python 环境,从 conda 仓库安装很多软件包,解决包依赖冲突等。poetry 是一个更侧重于项目管理的工具,除了虚拟环境、包依赖功能,还可以打包发布。pyvenv 则专注于虚拟环境管理。它可以同时支持 conda 和 poetry 创建的虚拟环境。

因此 conda 更适合作为一个基础系统,不适合放到 emacs 里使用。 poetry 作为自己开发项目的工具。pyvenv 管理 emacs 所用的 python 环境。

(use-package conda
  :defer 10
  :config
  (conda-env-initialize-interactive-shells)
  (conda-env-initialize-eshell)
  :commands (conda-env-activate conda-env-deactivate conda-env-list)
  :hook
  ;; set conda-project-env-path in .dir-locals.el then the env will be automatically activated
  (python-mode . conda-env-autoactivate-mode)
  )
(use-package poetry :defer 12)

conda 的虚拟环境是统一放在一个目录下的;而我喜欢配置 poetry 将虚拟环境放到项目目录下。通过 pyvenv-workon 可以从 conda 的虚拟环境目录里选择一个来激活;通过 pyvenv-activate 可以让我选择一个包含虚拟环境的目录来激活。这个包含虚拟环境的目录,可以是 poetry-venv-workon 创建的,也可以是 pyvenv-create 创建的。很方便。

有一点需要特别注意: poetry-shell 使用当前的虚拟环境,如果当前虚拟环境是 conda 创建的,会因为没有 activate.fish 而无法激活。因此需要使用 poetry-venv-workon 来激活虚拟环境。 在执行 poetry shell 前,一定要先激活环境。

另一个注意事项:pyvenv 切换环境,并不影响在切换前打开的 eshell。因此在 eshell 里需要手工 source activate.fish 进行环境切换。

(use-package pyvenv
  :ensure t
  :config
  (setenv "WORKON_HOME" (expand-file-name "~/.miniconda3/envs"))
  (pyvenv-mode t))
;; pip install jedi flake8 autopep8 black yapf
(use-package elpy
  :ensure t
  :init
  (advice-add 'python-mode :before 'elpy-enable)
  :custom
  (py-split-windows-on-execute-p nil)
  )

在编辑 tests 测试文件时, elpy 似乎有些错误。因此我尝试一下 jedi,但是 jedi 的补全看起来挺混乱的,经过测试,发现是 jedi 使用的 auto-complete 和 company-mode 冲突了。只好忍痛弃用。

(use-package jedi
  :custom
  (jedi:complete-on-dot t)
  :hook
  (python-mode . jedi:setup)
  )

在 Emacs 中执行 jedi:install-server 来安装 jedi server。

(use-package company-jedi
  :defer t
  :init
  (defun enable-jedi()
    (setq-local company-backends
                (append '(company-jedi) company-backends)))
  (with-eval-after-load 'company
    (add-hook 'python-mode-hook 'enable-jedi)))

emacs-jupyter 模拟 jupyter 环境。但是似乎不支持 markdown cell。命令: jupyter-run-repl 。 有一个问题,在 mitmproxy 源码目录下(非子目录),执行这个 jupyter-run-repl 不成功。另外,这个功能似乎和 run-python 差不多,有些鸡肋。

(use-package jupyter
  :custom
  (jupyter-repl-echo-eval-p t))

EIN 提供了一个 jupyter like 的环境。命令: ein:runein:login 。我只是还不知道如何写 markdown cell。感觉似乎不如 emacs-jupyter 好用。EIN 比 jupyter 依赖少。

(use-package ein)
(use-package go-mode
  :mode ("\\.go\\'" . go-mode)
  :init
  (setenv "GOROOT" "/usr/lib/go-1.22")
  )

用来用去,我觉得还是 org mode + python babel 比较好。

(use-package rust-mode
  :defer t
  :custom
  (rust-format-on-save t)
  (lsp-rust-server 'rust-analyzer))

下面是 lsp 配置。首先要先安装 lsp server。

# for C/C++ clangd
brew install llvm
# for python
python3 -m pip install "python-lsp-server[all]"
# or use pyright
python3 -m pip install pyright
# for golang
go install golang.org/x/tools/gopls@latest

这里说,使用 pyright . 作为 project-compile 的命令,可以做 python 项目的 linting。

(use-package lsp-pyright
  :hook
  (python-mode . (lambda ()
           (require 'lsp-pyright)
           (lsp-deferred))))
(use-package eglot
  :defer 3
  :bind (("C-c e e" . eglot)
         ("C-c e l" . eglot-list-connections)
         :map eglot-mode-map
         ("C-c e a" . eglot-code-actions)
         ("C-c e o" . eglot-code-actions-organize-imports)
         ("C-c e r" . eglot-rename)
         ("C-c e d" . eglot-find-declaration)
         ("C-c e f" . eglot-format))
  :hook
  (go-mode . eglot-ensure)
  )

没有 compile_commands.json 时,clangd 会占用大量的 CPU。因此我一般手动启用 eglot 和关闭。这时候 ggtags 非常有用。如果要设置 lsp server 参数,可以通过 .dir-locals.el 添加相关配置。下面是一个示例:

((nil . ((eglot-server-programs
          . (((c-mode c-ts-mode c++-mode c++-ts-mode objc-mode)
              . ("clangd"
                 "--compile-commands-dir=cmake-build"
                 "--all-scopes-completion"
                 "--background-index"
                 "--clang-tidy"
                 "--fallback-style=LLVM"
                 "--function-arg-placeholders=false"
                 "--header-insertion=iwyu"
                 "--header-insertion-decorators"
                 "--pretty"
                 "--enable-config"
                 "--pch-storage=memory"
                 "-j=2")))))))
(use-package ggtags
  :bind (:map ggtags-mode-map
              ("C-c g s" . ggtags-find-other-symbol)
              ("C-c g h" . ggtags-view-tag-history)
              ("C-c g r" . ggtags-find-reference)
              ("C-c g c" . ggtags-create-tags)
              ("C-c g u" . ggtags-update-tags)
              ("C-c g ." . ggtags-find-tag-dwim)
              ("C-c g ," . pop-tag-mark))
  :custom
  ;; smart-mode-line already provides project name on mode line
  (ggtags-mode-line-project-name nil)
  ;; while, ggtags consumes much CPU
  ;; :hook ((c-mode c++-mode) . ggtags-mode)
  )

dumb-jump 是在缺少 language server 的情况下支持定义跳转的工具。

(use-package dumb-jump
  :config
  (setq dumb-jump-disable-obsolete-warnings t)
  (setq dumb-jump-project-denoters '(".dumbjump" ".projectile" ".git" ".hg" ".fslckout" ".bzr" "_darcs" ".svn" "Makefile" "PkgInfo" "-pkg.el" "_FOSSIL_"))
  (setq xref-show-definitions-function #'xref-show-definitions-completing-read)
  (add-hook 'xref-backend-functions  #'dumb-jump-xref-activate)
  ;; :bind (("C-c j g" . dumb-jump-go)
  ;;        ("C-c j b" . dumb-jump-back)
  ;;        ("C-c j j" . dumb-jump-quick-look))
  )
;; from https://emacs.stackexchange.com/questions/78868/how-to-install-common-lisp-with-emacs-and-slime-when-the-slime-helper-el-is-not
(use-package slime
  :init
  (progn
    (require 'slime-autoloads)
    (add-hook 'slime-mode-hook
              (lambda ()
                (unless (slime-connected-p)
                  (save-excursion (slime))))))
  :config
  (progn
    (use-package slime-company)
    (setf inferior-lisp-program "sbcl")
    (slime-setup '(slime-fancy slime-company))
    (setq slime-net-coding-system 'utf-8-unix)
    (define-key lisp-mode-map (kbd "C-c C-q") 'slime-close-all-parens-in-sexp)
    (define-key slime-mode-indirect-map (kbd "M-_") 'paredit-convolute-sexp)
    (define-key slime-repl-mode-map (kbd "C-c C-z") #'quit-window)
    ))
(use-package rime
  :custom
  (default-input-method "rime")
  (rime-show-candidate 'posframe)
  )

关于 tree-sitter

这里有比较详细的配置说明。主要过程是:

  1. 使用 (treesite-available-p) 来测试是否可用
  2. 这里下载对应平台版本的解析器,解压到 treesit-extra-load-path 或者 ~/.emacs.d/tree-sitter
  3. 通过这个命令 fd -t f dylib --exclude 'libtree*' --exec mv {} libtree-sitter-{/} \; 把文件重命名
  4. 通过 (treesit-language-available-p 'c) 测试对应语言解析器进行测试
  5. 通过下面的配置,匹配原 major mode 到新的 ts mode
(when (treesit-available-p)
  (setq major-mode-remap-alist
      '((yaml-mode . yaml-ts-mode)
        (sh-mode . bash-ts-mode)
        (js-mode . js-ts-mode)
        (js2-mode . js-ts-mode)
        (typescript-mode . typescript-ts-mode)
        (json-mode . json-ts-mode)
        (css-mode . css-ts-mode)
        (c-mode . c-ts-mode)
        (c++-mode . c++-ts-mode)
        (c-or-c++-mode . c-or-c++-ts-mode)
        (python-mode . python-ts-mode)
        (go-mode . go-ts-mode))))

但是我还不知道 tree-sitter 的具体用法。另外,开启这个 python-mode 到 python-ts-mode 的映射后,elpy 不能正常工作,即使我把 elpy 配置里的 python-mode 换成 python-ts-mode。

关于 copilot

copilot 不能说好用,只能说很上瘾,尤其是问答的时候。

首先, git clone git@github.com:zerolfx/copilot.el~/.emacs.d 下。

(use-package s)
(use-package dash)
(use-package editorconfig)
(use-package copilot
  :load-path (lambda () (expand-file-name "copilot.el" user-emacs-directory))
  :config
  (setq copilot-max-char -1)
  (setq copilot-indent-offset-warning-disable t)
  :bind
  (("s-/" . copilot-accept-completion-by-line)))

需要执行 M-x copilot-install-serverM-x global-copilot-mode 。还需要 M-x copilot-login 。 有一个问题是,重启 Emacs 后,copilot mode 没有启用,直接要执行 global-copilot-mode 也没找到这个命令。只有在执行了一下 "s-/" 也就是 copilot-accept-completion-by-line 后,才有这个命令。我估计是自己的配置问题。

另外有一个包 codeium 提供类似功能,不过依赖的不是 github 服务而是一个专有二进制软件。另外还有一个项目叫 lsp-ai,也是类似的功能。有时间研究一下。

Org Mode

这个必不可少。

Basic Org Mode Configuration and Appearance

(use-package org
  :config
  (setq font-lock-ensure t)
  (setq org-log-done 'time)
  (setq org-log-into-drawer "LOGBOOK")
  (setq-default org-display-custom-times t)
  (setq org-time-stamp-custom-formats '("<%Y-%m-%d>" . "<%Y-%m-%d %H:%M>"))
  (setq org-startup-indented t)
  (setq org-return-follows-link t)
  (setq org-pretty-entities t)
  (setq org-pretty-entities-include-sub-superscripts t)
  (setq org-hide-emphasis-markers t)
  (setq org-agenda-block-separator "")
  (setq org-fontify-whole-heading-line t)
  (setq org-fontify-done-headline t)
  (setq org-fontify-quote-and-verse-blocks t)
  (setq org-startup-with-inline-images t)
  ;; (setq org-image-actual-width '(500))
  (setq org-startup-folded 'showall)
  (setq org-edit-src-content-indentation 0)
  (setq org-html-link-org-files-as-html t)
  (setq org-src-fontify-natively t)
  (setq calendar-week-start-day 1)

  (setq org-src-window-setup 'current-window) ;; or 'split-window-right
  (setq org-confirm-babel-evaluate nil)

  (setq org-directory "~/org")
  (setq org-directory-publish "~/www")
  (setq my-work-notes-directory "~/work/notes")
  (setq my-work-notes-publish-directory "~/work/publish")

  (add-hook 'org-babel-after-execute-hook 'org-redisplay-inline-images))
(use-package org-bullets
  :defer 20
  :hook (org-mode . org-bullets-mode)
  :config
  (setq org-bullets-bullet-list '("◉" "⁑" "⁂" "❖" "✮" "✱" "✸"))
  (setq org-ascii-bullets '((ascii ?* ?+ ?-) (latin1 ?* ?+ ?-) (utf-8 ?* ?+ ?-))))

(use-package org-appear
  :config
  (setq org-appear-autolinks t)
  :hook (org-mode . org-appear-mode))

;; it requires emacs 28
(use-package org-reverse-datetree)

Org Mode Babel

先安装 mermaid:

npm install -g @mermaid-js/mermaid-cli
(use-package mermaid-mode
  :commands mermaid-mode
  :mode "\\.mmd\\'"
  :init
  (setq mermaid-output-format ".svg")
  )

(use-package ob-mermaid
  :defer 5
  :commands
  (org-babel-execute:mermaid)
  :config
  (setq ob-mermaid-cli-path "~/.node/bin/mmdc")
  )

mermaid 生成的 svg 图片正常,但是在 emacs 中展示时,有些文字无法显示出来。这是我在 macOS 下遇到的情形。不知道在 Linux 下是否有这个问题。

下面是个示例:

mermaid-demo.png

我总觉得 plantuml 比较慢啊。

(use-package plantuml-mode
  :defer 10
  :custom
  (plantuml-default-exec-mode 'executable)
  ;; (plantuml-jar-path "/usr/share/plantuml/plantuml.jar")
  :mode "\\.uml\\'")
(use-package ob-plantuml
  :defer 8
  :ensure org-contrib
  :commands
  (org-babel-execute:plantuml)
  :custom
  (org-plantuml-exec-mode 'plantuml)
  (org-plantuml-executable-path "plantuml")
  (org-plantuml-executable-args '("-headless" "-charset UTF-8"))
  )

这是个 plantuml 示例:

plantuml-sequence-demo.png
(use-package ob-python
  :defer 5
  :ensure org-contrib
  :commands
  (org-babel-execute:python))

如果使用 :session ,则需要 return 一个结果;如果不用 :session,则可以直接返回一个顶层变量:

import matplotlib
import matplotlib.pyplot as plt
fig=plt.figure(figsize=(3, 2))
plt.plot([1, 3, 2, 6])
fig.tight_layout()

fname = 'python-matplot-demo.jpg'
plt.savefig(fname)
return fname
python-matplot-demo.jpg

使用了相同 :session 的代码块,可以共享变量。

One hint: every time you make any changes to the file, these code blocks are evaluated. It is annoying. If they are good to go, you can avoid further evaluations by adding header arguments like :noeval or :eval no.

Org Mode Export

我主要把 org 文件导出为 html:

(use-package htmlize
  :defer t
  :commands (org-export-dispatch))

按照官方文档以及网上的大多教程,org 文件按照目录组织成项目的形式:

(use-package org
  :config
  (defvar my-publish-html-head
    "<link rel='icon' type='image/x-icon' href='/images/favicon.ico'/>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<link rel='stylesheet' href='/css/style.css' type='text/css'/>
<link rel='stylesheet' href='/css/syntax-coloring.css' type='text/css'/>")

  (defvar my-publish-html-preamble
    "<nav><a href=\"#\"><div id=\"arrow-up\"></div></a><br>
     <a href=\"/index.html\"><div id=\"home\"></div></a><br>
     <a href=\"/about.html\"><div id=\"link\"></div></a></nav>")

  (defvar my-publish-html-postamble
    (format "<br/><hr/>&copy; 2012-%s All Rights Reserved.<br/><br/>"
            (format-time-string "%Y")))

  (setq org-export-global-macros
        '(("timestamp" . "@@html:<span class=\"timestamp\">[$1]</span>@@")))

  (defun my-sitemap-date-entry-format (entry style project)
    "Format ENTRY in org-publish PROJECT Sitemap format ENTRY ENTRY STYLE format that includes date."
    (let ((filename (org-publish-find-title entry project)))
      (if (= (length filename) 0)
          (format "*%s*" entry)
        (format "{{{timestamp(%s)}}} [[file:%s][%s]]"
                (format-time-string "%Y-%m-%d"
                                    (org-publish-find-date entry project))
                entry
                filename))))
  (require 'ox-publish)
  (require 'ox-md nil t) ;; for markdown export
  (setq org-html-divs '((preamble "header" "top")
                        (content "main" "content")
                        (postamble "footer" "postamble"))
        org-html-container-element "section"
        org-html-metadata-timestamp-format "%b %d, %Y"
        org-html-checkbox-type 'html
        org-html-html5-fancy t
        org-html-validation-link t
        org-html-doctype "html5"
        org-html-htmlize-output-type 'css
        org-html-mathjax-options
        '((path "/js/mathjax/tex-chtml.js?config=TeX-MML-AM_CHTML")
          (align "center")
          (indent "2em")
          (mathml nil))

        org-export-with-toc 3
        org-export-headline-levels 3
        org-export-with-section-numbers t
        org-export-with-smart-quotes t
        org-export-with-sub-superscripts '{}
        org-export-allow-bind-keywords t ;; it's dangerous to enable this, anyway.
        )

  (setq org-publish-project-alist
        `(
          ("notes"
           :base-directory ,org-directory
           :base-extension "org"
           :publishing-directory ,org-directory-publish
           :recursive t
           :publishing-function org-html-publish-to-html
           :auto-sitemap t
           :sitemap-title "All Posts"
           :sitemap-filename "posts.org"
           :sitemap-style list
           :sitemap-sort-files anti-chronologically ;; newer files first
           :sitemap-format-entry my-sitemap-date-entry-format
           :author ,user-full-name
           :email ,user-mail-address
           :with-creator t
           :exclude ,(regexp-opt '("archive" "diary" "dotfiles" "css" "js" "images"))
           :exclude-tags ("noexport" "todo")
           :html-preamble ,my-publish-html-preamble
           :html-postamble ,my-publish-html-postamble
           :html-head ,my-publish-html-head
           :html-doctype "html5"
           :html-html5-fancy t
           :html-head-include-scripts nil
           :html-head-include-default-style nil
           )
          ("notes-static"
           :base-directory ,org-directory
           :base-extension "css\\|js\\|svg\\|png\\|jpg\\|gif\\|ico\\|pdf\\|mp3\\|ogg\\|swf"
           :publishing-directory ,org-directory-publish
           :recursive t
           :publishing-function org-publish-attachment
           :exclude ,(regexp-opt '("archive" "draft" "diary" "resume" "snippets"))
           )
          ("worklog"
           :base-directory ,my-work-notes-directory
           :base-extension "org"
           :publishing-directory ,my-work-notes-publish-directory
           :recursive t
           :with-drawers t
           :publishing-function org-html-publish-to-html
           :auto-sitemap t
           :sitemap-title "Work Logs"
           :sitemap-filename "index.org"
           :sitemap-style list
           :author ,user-full-name
           :email ,user-mail-address
           :with-creator t
           )
          ("worklog-static"
           :base-directory ,my-work-notes-directory
           :base-extension "css\\|js\\|svg\\|png\\|jpg\\|gif\\|pdf\\|mp3\\|ogg\\|swf"
           :publishing-directory ,my-work-notes-publish-directory
           :recursive t
           :publishing-function org-publish-attachment
           )
          ("gtd" :components ("notes" "notes-static"))
          ("work" :components ("worklog" "worklog-static"))
          ))
  )

我不太喜欢这种方式,因为文件必须在特定目录下,或者新添加 project。我喜欢用 hook 保存时自动 export(就是下面的配置)。两种方式各有利弊。

(use-package org
  :config

  (setq org-html-preamble t)
  (setq org-html-postamble t)
  (setq org-html-preamble-format '(("en" "<nav><a href=\"#\"><div id=\"arrow-up\"></div></a><br>
     <a href=\"/index.html\"><div id=\"home\"></div></a><br>
     <a href=\"/about.html\"><div id=\"link\"></div></a></nav>")))
  (setq org-html-postamble-format `(("en"
                                     ,(format "<br/><hr/>&copy; 2012-%s All Rights Reserved.<br/><br/>"
                                              (format-time-string "%Y"))
                                     )))
  (setq org-html-head
        "<link rel='icon' type='image/x-icon' href='/images/favicon.ico'/>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<link rel='stylesheet' href='/css/style.css' type='text/css'/>
<link rel='stylesheet' href='/css/syntax-coloring.css' type='text/css'/>")

  (setq org-export-global-macros
        '(("timestamp" . "@@html:<span class=\"timestamp\">[$1]</span>@@")))

  (require 'ox-publish)
  (require 'ox-md nil t) ;; for markdown export
  (setq org-html-divs '((preamble "header" "top")
                        (content "main" "content")
                        (postamble "footer" "postamble"))
        org-html-container-element "section"
        org-html-metadata-timestamp-format "%b %d, %Y"
        org-html-checkbox-type 'html
        org-html-html5-fancy t
        org-html-validation-link t
        org-html-doctype "html5"
        org-html-htmlize-output-type 'css
        org-html-mathjax-options
        '((path "/js/mathjax/tex-chtml.js?config=TeX-MML-AM_CHTML")
          (align "center")
          (indent "2em")
          (mathml nil))

        org-export-with-toc 3
        org-export-headline-levels 3
        org-export-with-section-numbers t
        org-export-with-smart-quotes t
        org-export-with-sub-superscripts '{}
        org-export-allow-bind-keywords t ;; it's dangerous to enable this, anyway.
        )

  (defun my-org-save-hook ()
    (when (eq major-mode 'org-mode)
      (org-html-export-to-html)))
  :hook
  (after-save . my-org-save-hook)
  )

用 org mode 写网页 Presentation:

(use-package ox-reveal
  :commands (org-export-dispatch)
  :config
  (setq org-reveal-root "https://cdn.jsdelivr.net/npm/reveal.js")
  (setq org-reveal-mathjax t))

Getting Things Done

这个是一个 org-capture-templates 的例子,可以研究一下:

(setq org-capture-templates
    '(("t" "Tasks / Projects")
      ("tt" "Task" entry (file+olp "~/org/demo/Tasks.org" "Inbox")
           "* TODO %?\n  %U\n  %a\n  %i" :empty-lines 1)

      ("j" "Journal Entries")
      ("jj" "Journal" entry
           (file+olp+datetree "~/org/demo/Journal.org")
           "\n* %<%I:%M %p> - Journal :journal:\n\n%?\n\n"
           ;; ,(dw/read-file-as-string "~/Notes/Templates/Daily.org")
           :clock-in :clock-resume
           :empty-lines 1)
      ("jm" "Meeting" entry
           (file+olp+datetree "~/org/demo/Journal.org")
           "* %<%I:%M %p> - %a :meetings:\n\n%?\n\n"
           :clock-in :clock-resume
           :empty-lines 1)

      ("jd" "Daily Journal" entry
           (file+olp+datetree "~/org/demo/Journal.org")
           "* %<%I:%M %p> - %a :diary:\nTell me your name?\n\n%^{name}\nHow old are you?\n\n%^{age}\nwhat about your moode?\n\n%^{mood}\n\n"
           :clock-in :clock-resume
           :empty-lines 1)

      ("w" "Workflows")
      ("we" "Checking Email" entry (file+olp+datetree "~/org/demo/Journal.org")
           "* Checking Email :email:\n\n%?" :clock-in :clock-resume :empty-lines 1)

      ("m" "Metrics Capture")
      ("mw" "Weight" table-line (file+headline "~/org/demo/Metrics.org" "Weight")
       "| %U | %^{Weight} | %^{Notes} |" :kill-buffer t)))
(use-package org
  :config
  ;; my personal file, for study notes and diary
  (setq org-default-notes-file (concat org-directory "/gtd.org"))
  (setq my-diary-file (concat org-directory "/diary/diary.org"))
  (setq my-trade-file (concat org-directory "/diary/trade.org"))
  (setq my-journal-index-file (concat org-directory "/journals/index.org"))
  (setq my-publish-index-file (concat org-directory "/index.org"))
  ;; my work related file
  (setq my-work-task-file (concat my-work-notes-directory "/work.org"))

  (setq org-agenda-files (mapcar 'file-truename (list
                                                 org-default-notes-file
                                                 my-work-task-file
                                                 )))

  (setq org-refile-targets '((org-default-notes-file :level . 1)
                             (my-work-task-file :level . 1)))
  ;; configure org-capture templates
  (setq org-capture-templates
        '(("d" "Diary" entry (file+datetree my-diary-file) "* %<%H:%M>\n%?\n" :empty-lines 1 :kill-buffer t)
          ("t" "Trade" entry (file+datetree my-trade-file) "* %<%H:%M>\n%?\n" :empty-lines 1 :kill-buffer t)
          ("p" "Pomodoro" entry (file+headline org-default-notes-file "Pomodoro") "* TODO %?\n" :empty-lines 1 :prepend t)
          ("i" "Inbox" entry (file+headline org-default-notes-file "Inbox") "* %?\n" :empty-lines 1 :prepend t)
          ("j" "Journal" item (file+headline my-journal-index-file "Journals") "- %<%Y-%m-%d> %?\n" :empty-lines 1 :prepend t)
          ("w" "Work" entry (file+headline my-work-task-file "Tasks") "* TODO %<%Y-%m-%d %H:%M> %?\n" :empty-lines 1)))
  (setq org-reverse-note-order t)

  ;; (setq org-todo-keywords
  ;;       '((sequence "TODO(t!)" "ON GOING(g!)" "DONE(d@/!)")))

  (setq org-todo-keywords
        '((sequence "TODO(t!)" "ON GOING(g!)" "DONE(d!)")))

  (setq org-todo-keyword-faces
        '(("TODO" . "red")
          ("ON GOING" . (:foreground "purple" :weight bold))
          ("DONE" . "green")))

  ;; (setq org-tag-alist '(("work" . ?w) ("study" . ?s) ("coding" . ?c)))
  (setq org-link-frame-setup '((vm . vm-visit-folder-other-frame)
                               (vm-imap . vm-visit-imap-folder-other-frame)
                               (gnus . org-gnus-no-new-news)
                               (file . find-file)
                               (wl . wl-other-frame)))
  :bind
  (("C-c d" . org-cycle-agenda-files) ;; make it global
   ("C-c c" . org-capture)
   ("C-c a" . org-agenda)
   :map org-mode-map
   ("C-," . nil)
   ("C-'" . nil)))

(defun my-publish-index-org (file)
  "Export one org file to html in the background"
  (find-file file)
  (org-html-export-to-html)
  (previous-buffer)
  )

(defun my-org-capture-finalize-hook ()
  "Export ~/org/index.org as the file it contains ~/org/journals/index.org would be modified by org capture"
  (my-publish-index-org my-publish-index-file)
  )

;; export related org files in the background after the org capture
(add-hook 'org-capture-after-finalize-hook 'my-org-capture-finalize-hook)

(defun gtd-save-org-buffers ()
  "Save `org-agenda-files' buffers without user confirmation. See also `org-save-all-org-buffers'"
  (interactive)
  (message "Saving org-agenda-files buffers...")
  (save-some-buffers t (lambda ()
                         (when (member (buffer-file-name) org-agenda-files)
                           t)))
  (message "Saving org-agenda-files buffers... done"))

;; save change after refile
(advice-add 'org-refile :after
            (lambda (&rest _)
              (gtd-save-org-buffers)))

(use-package org
  :config
  (setq org-agenda-start-on-weekday 1
        org-agenda-include-diary t
        org-agenda-inhibit-startup t ;; ~50x speedup
        org-agenda-use-tag-inheritance nil ;; 3-4x speedup
        org-clock-in-switch-to-state "ON GOING"
        ;; Save clock data and notes in the LOGBOOK drawer
        org-clock-into-drawer t
        ;; Removes clocked tasks with 0:00 duration
        org-clock-out-remove-zero-time-clocks t)
  )

(use-package org-pomodoro
  :init
  (setq org-pomodoro-format "💪%s")
  (setq org-pomodoro-short-break-format "👓%s")
  (setq org-pomodoro-long-break-format "💧%s")
  (setq org-pomodoro-play-sounds nil)
  (setq org-pomodoro-manual-break nil) ;; or t if interruptions is not expected
  (setq org-pomodoro-keep-killed-pomodoro-time t)
  (setq org-pomodoro-length 25)
  (setq org-pomodoro-short-break-length 5)
  (setq org-pomodoro-long-break-length 20)
  (setq org-pomodoro-long-break-frequency 4)
  (setq org-clock-clocked-in-display nil) ;; display in header line; see below
  :bind ("C-c p" . org-pomodoro)

  :config
  ;; TODO make a platform independent notification
  (defun pomodoro-notifier (msg)
    (split-window-right)
    (animate-sequence msg 2)
    (select-window (previous-window))
    (gtd-save-org-buffers)
    )
  (add-hook 'org-pomodoro-finished-hook
            (lambda ()
              (pomodoro-notifier (list "已经工作很长时间啦!" "歇歇眼睛,休息一下吧!"))))
  (add-hook 'org-pomodoro-break-finished-hook
            (lambda ()
              (pomodoro-notifier (list "千里之行,始于足行。" "不积跬步,无以至千里。" "休息完毕,开启新的征程吧!"))))

  (defun with-face (str &rest face-plist)
    (propertize str 'face face-plist))

  ;; Show the clocked-in task - if any - in the header line
  (defun my-show-org-clock-in-header-line ()
    (setq-default header-line-format '((" Current Task: "
                                        (:eval (with-face org-mode-line-string
                                                          :background "forest green"
                                                          :foreground "purple"
                                                          :weight 'bold
                                                          ))
                                        )))
    (gtd-save-org-buffers)
    )

  (defun my-hide-org-clock-in-header-line ()
    (setq-default header-line-format nil)
    (gtd-save-org-buffers))
  (add-hook 'org-clock-in-hook 'my-show-org-clock-in-header-line)
  (add-hook 'org-clock-out-hook 'my-hide-org-clock-in-header-line)
  (add-hook 'org-clock-cancel-hook 'my-hide-org-clock-in-header-line)
  ;; use C-c C-x C-j to jump back the current org clock entry
  ;; use C-u C-c p to resume the task
  )

一些有用的工具

Reading in Emacs:

(use-package pdf-tools
  :if (display-graphic-p)
  :init
  (pdf-loader-install)
  :config
  (setq-default pdf-view-display-size 'fit-width)
  (add-hook 'pdf-view-mode-hook (lambda () (display-line-numbers-mode -1)))
  :custom
  (pdf-annot-activate-created-annotations t "automatically annotate highlights")
  )

(use-package pdf-view-restore
  :if (display-graphic-p)
  :after pdf-tools
  :config
  (setq pdf-view-restore-filename "~/.emacs.d/.pdf-view-restore")
  (add-hook 'pdf-view-mode-hook 'pdf-view-restore-mode))
(use-package emacs
  :hook
  (after-init . pixel-scroll-precision-mode))

Or use good-scroll if emacs version is lower than 29:

(use-package good-scroll
  :ensure t
  :if window-system
  :init (good-scroll-mode))
(use-package vterm
  :custom
  (vterm-buffer-name-string "vterm %s")
  :config
  (add-hook 'vterm-mode-hook (lambda ()
                               (display-line-numbers-mode -1)
                               (global-hl-line-mode -1)
                               ))
  )

(use-package vterm-toggle
  :custom
  (vterm-toggle-fullscreen-p t "Open a vterm in another window.")
  (vterm-toggle-scope 'project)
  :bind (("C-c t" . #'vterm-toggle)))

Eshell configuration:

(setq
      eshell-save-history-on-exit   t
      eshell-history-size           512
      eshell-hist-ignoredups        t
      eshell-cmpl-ignore-case       t
      eshell-cp-interactive-query   t
      eshell-ln-interactive-query   t
      eshell-mv-interactive-query   t
      eshell-rm-interactive-query   t
      eshell-mv-overwrite-files     nil
      eshell-highlight-prompt   t
      eshell-prompt-regexp      "^[^#$\n]* [#>]+ "
      eshell-prompt-function    (lambda nil
                                  (concat
                                   (abbreviate-file-name
                                    (eshell/pwd))
                                   (if
                                       (=
                                        (user-uid)
                                        0)
                                       " # " " >>> ")))
)

ERC and Gnus

(use-package erc
  :config
  (setq erc-nick "hexingb" erc-user-full-name user-full-name)
  ;; (erc-autojoin-mode 1)
  (setq erc-try-new-nick-p nil)
  (setq erc-autojoin-channels-alist '(("irc.libera.chat" "##programming")))
  (setq erc-ignore-list nil)
  (setq erc-hide-list '("JOIN" "PART" "QUIT" "MODE"))
  (setq erc-server-reconnect-attempts 50
        erc-server-reconnect-timeout 3
        erc-fill-static-center 22
        erc-join-buffer 'bury
        erc-kill-buffer-on-part t)
  (require 'erc-log)
  (erc-log-mode 1)
  (setq erc-default-coding-system '(utf-8 . utf-8))
  (setq erc-log-channels-directory "~/.erc/logs/"
        erc-save-buffer-on-part t
        erc-log-file-coding-system 'utf-8
        erc-log-write-after-send t
        erc-log-write-after-insert t)
  (unless (file-exists-p erc-log-channels-directory)
    (mkdir erc-log-channels-directory t))
  (defadvice save-buffers-kill-emacs (before save-logs (arg) activate)
    (save-some-buffers t (lambda ()
                           (when (and (eq major-mode 'erc-mode)
                                      (not (null buffer-file-name)))))))
  )
(setq gnus-select-method '(nntp "news.eternal-september.org"))

for authentication, create file ~/.authinfo and add following content:

machine news.eternal-september.org login your_account_name force yes password your_account_password

这里,需要在加载完所有 package 之后再读取以前保存的文件,防止如 elpy 之类出错。

(use-package emacs
  :config
  (desktop-read))

(use-package spacious-padding
  :custom
  (spacious-padding-widths
   '(
     :internal-border-width 15
     :header-line-width 4
     :mode-line-width 3
     :tab-width 4
     :right-divider-width 30
     :scroll-bar-width 8
     :fringe-width 8))
  :hook
  (after-init . spacious-padding-mode))

其他有用的包

  1. keycast for key casting
  2. org-download for image capturing into org documents
  3. beacon, highlight cursor when jump window/buffer
  4. rainbow-delimiters and rainbow-identifiers
  5. call-graph for function calling stack graph
  6. highlight-indent-guides, Highlight indent guides for programming
  7. hydra

TODO 添加更多配置

  1. flymake
  2. gpg 配置后,打开 gpg.org 文件总提示编码问题
  3. irc
  4. gnus
  5. 添加更多编程语言支持

遇到的问题

org 文件导出的时候,无法将生成的 html 文件,转移到一个指定的目录去

目前没有解决方案。如上配置,当保存 org 文件时,自动执行 export。我在自己的笔记项目目录下写了一个 Makefile 来拷贝文件。

org 文件导出为 markdown 的时候,会生成一些奇奇怪怪的 id,如何去掉这些 id ?

算了,markdown 文件只为协同沟通,还是直接写吧。

org 文件导出为 markdown 的时候,a.md 文件里链接了 b.md, export a.md 的时候,生成的 a.html 里的链接仍是 b.md 而不是 b.html

我估计这个要看 pandoc 的支持,目前没有找到解决方案。

重新配置 emacs 下载了最新版本的 magit-20231014.1408,结果和 emacs 28.2 不适配,导致 magit 切换分支不成功,查看 log 有乱码

回退到 magit-20230723.1601 恢复正常。这里有说明。

References

我参考了太多了的网络文章,这里就只感谢,不列举了。你喜欢什么,你就能得到什么。