基于 Nix Flakes 的 Emacs 配置

Table of Contents

1. 本配置

1.1. 目的

这份配置文件的特色是使用 Nix Flakes 同时管理 emacs 的包和 emacs 在使用过程中会调用的外部程序。使用这个方法的主要目的是有以下理由:

同时协同 emacs 插件和其依赖的外部工具
emacs 内置的包管理器无法管理系统级别工具,比如用于检索的 consult-ripgrep 会寻找可运行的 ripgrep 调用。这样我需要脱离 elisp 的设定文件去下载这个外部工具。还有很多编程语言也有这个问题。我的习惯是希望针对特定的项目文件夹定制所需的环境(比如使用 nix 和.dir-locals.el)。但是,类似于语言服务器这类本质与项目无关,但与我的编辑环境有关的设定,我倾向于使用与项目无关的环境。

1.2. 其他方式做不到的

目前不依赖 emacs 的其他包管理工具的其他方式也有一些:

git 子模块
这种方式可以管理 emacs 包,而且可以灵活的更新和回滚版本。但是这个方法也同样不方便整合系统级别的依赖。
twist.nix 等基于 nix 的方法
在 nix 文件里写配置的话,确实可以使用 ${pkgs.xxx} 的方式使用系统工具避免,在配置文件外单独下载。但完全使用 nix 的话,写配置的方式更新快,个人希望尽可能使用 elisp 的方式写配置。

1.3. 本配置的做法

这份配置的解决法如下:

  • 通过 org-babel 同时写 nix 和 elisp。前者用于版本,emacs 包,依赖的系统工具的管理,后者用于 emacs 本身的配置的管理。
  • 可以在 elisp 中用 $pkgs.git 的方式直接引用Nix的包。

1.4. 目前的缺点

目前使用下,这个配置方式也有一些缺点:

  • 添加系统工具的时候,需要重启调试过程繁琐
  • 依赖的系统工具多了以后,占用容量大
  • nixpkgs 里的工具相对滞后,新包加入慢

2. 实现方式

下面以配置 Magit 为例说明这个配置的方法。这里和大多数的配置一样我们把整体目的一致的包放在一个集群实现。每个集群主要分成两个子节点,一个用于安装包和依赖项,另一个用于配置包本身。

依赖的部分可以从 pkgs (nixpkgs.legacyPackages.${system}) 同时安装 emacs 外部依赖(系统工具如 git )。然后从 epkgs (pkgs.emacs.pkgs.withPackages) 中安装emacs 的包。

这里稍微多做一些解释。在配置节里面,我利用了org-babel的一些文学编程特性。首先我们在一个入口代码块中使用了 :noweb=yes 1。然后在后面的代码块中我们使用 :noweb-ref 参照入口代码块,最后后续的这些代码块都会在 org-babel-tangle (C-c C-v C-t) 时合并到这个入口代码块中输出为 flake.nix。

3. 如何使用

用emacs打开本文档, M-x org-babel-tangle 或使用快捷键 C-c C-v t 生成 flake.nix。接下去运行可以通过在终端运行下面进行调试和版本回滚:

3.1. 调试

export NIXPKGS_ALLOW_UNFREE=1	# allow unfree packages
nix run <path/to/this/config> \	# replace <path/to/this/config> with the actual path
    --impure \			# use impure mode
    --extra-experimental-features nix-command \ # enable nix-command feature
    --extra-experimental-features flakes # enable flakes feature
source ~/.bashrc			 # source bash configuration
ne			     # starts emacs with the new configuration

3.2. 版本回滚

3.2.1. 使用git

git checkout <previous-commit>
nix run <path/to/this/config> \	# replace <path/to/this/config> with the actual path
    --impure \			# use impure mode
    --extra-experimental-features nix-command \ # enable nix-command feature
    --extra-experimental-features flakes # enable flakes feature
source ~/.bashrc			 # source bash configuration
ne			     # starts emacs with the new configuration

3.2.2. 备份lock文件

nix flake metadata		# check flake metadata
cp flake.lock flake.lock.bachup	# backup current lock file
nix run --recreate-lock-file --inputs-from ./flake.lock.backup # recreate lock file

4. 配置入口 (flake.nix)

包的安装基于下面的 flake.nix 。可以看到这里使用了几个占位符:

<<epkgs>>
这里放置 emacs 包的列表
<<ecfg>>
这里放置 emacs 的配置代码
<<emacs-early-init-config>>
这里放置 emacs 的 early-init 配置代码
<<externals>>
这里放置外部包的定义

这些占位符会在后续的代码块中被替换。

{
  description = "idiig's Emacs Configuration with Nix Flakes";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    flake-utils.url = "github:numtide/flake-utils";
    emacs-overlay.url = "github:nix-community/emacs-overlay";

  };

  outputs = inputs@{ self, nixpkgs, flake-utils, emacs-overlay, ... }:
    let
      # 用于安装非 nixpkgs 元的外部包的函数
      mkPackages = pkgs: emacsPackages: import ./externals {
        inherit inputs pkgs emacsPackages;
      };
    in
      flake-utils.lib.eachDefaultSystem (system:
              let

                      pkgs = import nixpkgs {
                              inherit system;
                              overlays = [ emacs-overlay.overlay ];
                      };

                      # 主配置文件
                      emacsConfig = pkgs.writeText "init.el" ''
            ;; 在mac中使用Command key作为meta
            (setq mac-option-key-is-meta nil
                  mac-command-key-is-meta t
                  mac-command-modifier 'meta
                  mac-option-modifier 'none)

            ;; 便于使用mac的JIS日语键盘
            (global-set-key (kbd "C-¥") 'toggle-input-method)
            (require 'use-package)
            (require 'diminish)
            ;; 关闭警告声
            (setq ring-bell-function 'ignore)

            ;; 确认使用y或n,而不是yes或no。
            (defalias 'yes-or-no-p 'y-or-n-p)

            ;; 不自动生成备份文件
            (setq make-backup-files nil)

            ;; 选中文字能被整体替换(与其他文本编辑器相同)
            (delete-selection-mode 1)

            ;; 文件最后添加新行
            (setq require-final-newline t)

            ;; 文件在外部更新时buffer更新
            (global-auto-revert-mode 1)
            (use-package so-long
              :init
              (global-so-long-mode +1))
            ;; 基础设置
            (tool-bar-mode -1) ;; 关闭工具栏
            (scroll-bar-mode -1) ;; 关闭文件滑动控件
            (setq inhibit-splash-screen 1) ;; 关闭启动帮助画面
            (setq initial-frame-alist (quote ((fullscreen . maximized)))) ;; 全屏
            (setq initial-scratch-message nil) ;; 关闭scratch message
            (setq inhibit-startup-message t) ;; 关闭启动信息
            (setq frame-title-format
                  ;; 窗口显示文件路径/buffer名
                  '("" " idiig - "
                    (:eval (if (buffer-file-name)
                               (abbreviate-file-name (buffer-file-name)) "%b"))))
            (setq ns-use-proxy-icon nil)  ;; 删除frame icon
            (require-theme 'modus-themes)
            (use-package spacious-padding
              :config
              (setq spacious-padding-widths
                    '( :internal-border-width 15
                       :header-line-width 4
                       :mode-line-width 6
                       :tab-width 4
                       :right-divider-width 30
                       :scroll-bar-width 8))

              ;; Read the doc string of `spacious-padding-subtle-mode-line' as it
              ;; is very flexible and provides several examples.
              (setq spacious-padding-subtle-mode-line
                    `( :mode-line-active 'default
                       :mode-line-inactive vertical-border)))
            (setq switch-to-buffer-obey-display-actions t)
            (setq switch-to-buffer-in-dedicated-window 'pop)
            (customize-set-variable 'display-buffer-base-action
                                '((display-buffer-reuse-window display-buffer-same-window)
                                  (reusable-frames . t)))
            (defadvice split-window-below (after split-window-below-and-switch activate)
              "切换到新分割的窗口"
              (when (called-interactively-p 'any)
                (other-window 1)))

            (defadvice split-window-right (after split-window-right-and-switch activate)
              "切换到新分割的窗口"
              (when (called-interactively-p 'any)
                (other-window 1)))
            (global-set-key (kbd "C-x V") 'shrink-window)

            (defun idiig/window-adjust-advice (orig-fun &rest args)
              "使用 Emacs 风格按键 (^, V, {, }, +) 持续调整窗口大小。"
              (let* ((ev last-command-event)
                 (echo-keystrokes nil))
                ;; 执行初始调整
                (apply orig-fun args)

                ;; 设置 transient map
                (let ((delta (car args))) 
                  (set-transient-map
                   (let ((map (make-sparse-keymap)))
                 ;; 垂直调整
                 (define-key map (kbd "^")
                             `(lambda () (interactive) (enlarge-window ,delta nil)))
                 (define-key map (kbd "V")
                             `(lambda () (interactive) (shrink-window ,delta nil)))

                 ;; 水平调整
                 (define-key map (kbd "{")
                             `(lambda () (interactive) (shrink-window ,delta t)))
                 (define-key map (kbd "}")
                             `(lambda () (interactive) (enlarge-window ,delta t)))

                 ;; 平衡窗口
                 (define-key map (kbd "+")
                             (lambda () (interactive) (balance-windows)))
                 ;; 最大化窗口
                 (define-key map (kbd "M")
                             (lambda () (interactive) (maximize-window)))
                 ;; 最小化窗口
                 (define-key map (kbd "m")
                             (lambda () (interactive) (minimize-window)))
                 map)
                   nil nil
                   "Use %k for further adjustment"))))

            ;; 添加 advice
            (advice-add 'enlarge-window :around #'idiig/window-adjust-advice)
            (advice-add 'shrink-window :around #'idiig/window-adjust-advice)
            (advice-add 'enlarge-window-horizontally :around #'idiig/window-adjust-advice)
            (advice-add 'shrink-window-horizontally :around #'idiig/window-adjust-advice)
            (advice-add 'maximize-window :around #'idiig/window-adjust-advice)
            (advice-add 'minimize-window :around #'idiig/window-adjust-advice)
            ;; 不存在文档时询问是否新建
            (add-hook 'before-save-hook
                      (lambda ()
                        (when buffer-file-name
                          (let ((dir (file-name-directory buffer-file-name)))
                            (when (and (not (file-exists-p dir))
                                       (y-or-n-p (format "Directory %s does not exist. Create it?" dir)))
                              (make-directory dir t))))))

            ;; 找文件时若无母文档则新建 
            (defadvice find-file (before make-directory-maybe
                                         (filename &optional wildcards) activate)
              "Create parent directory if not exists while visiting file."
              (unless (file-exists-p filename)
                (let ((dir (file-name-directory filename)))
                  (when dir
                    (unless (file-exists-p dir)
                      (make-directory dir t))))))
            (use-package recentf
              :defer t
              :commands
              (consult-recent-file)
              :init
              (setq recentf-save-file (expand-file-name "recentf" user-emacs-directory)
                    recentf-max-saved-items 500
                    recentf-max-menu-items 10)
              (setq recentf-exclude
                    '("COMMIT_MSG"
                      "COMMIT_EDITMSG"
                      "github.*txt$"
                      "/tmp/"
                      "/sudo:"
                      "/TAGS$"
                      "/GTAGS$"
                      "/GRAGS$"
                      "/GPATH$"
                      "\\.mkv$"
                      "\\.mp[34]$"
                      "\\.avi$"
                      "\\.sub$"
                      "\\.srt$"
                      "\\.ass$"
                      ".*png$"
                      "Nutstore/org-files/"
                      "bookmarks"))
              (setq recentf-max-saved-items 2048)
              (recentf-mode 1))

            ;; cleanup recent files
            (defun idiig/cleanup-recentf ()
              (progn
                (and (fboundp 'recentf-cleanup)
                     (recentf-cleanup))))
            (add-hook 'kill-emacs-hook #'idiig/cleanup-recentf)
            (use-package savehist
              :init
              (setq savehist-additional-variables
                    ;; search entries
                    '(search-ring regexp-search-ring)
                    ;; 每一分钟保存一次
                    savehist-autosave-interval 60
                    ;; keep the home clean
                    savehist-file (expand-file-name "savehist" user-emacs-directory))
              (savehist-mode t))
            (use-package bookmark
              :init
              (setq bookmark-default-file (expand-file-name "bookmarks" user-emacs-directory)
                    bookmark-save-flag 1))
            (use-package saveplace
              :init
              (setq save-place-file (expand-file-name "place" user-emacs-directory))
              (save-place-mode 1))
            (use-package mwim
              :bind
              ("C-a" . mwim-beginning-of-code-or-line-or-comment)
              ("C-e" . mwim-end-of-code-or-line)
              :commands
              (mwim-beginning-of-code-or-line-or-comment
               mwim-end-of-code-or-line))
            (use-package unfill
              :bind
              ("M-q" . unfill-toggle)
              :commands
              (unfill-toggle))
            (use-package emacs
              :init
              (progn
                ;; 为`completing-read-multiple'添加提示,比如[CRM<separator>]
                (defun crm-indicator (args)
                  (cons (format "[CRM%s] %s"
                                (replace-regexp-in-string
                                 "\\`\\[.*?]\\*\\|\\[.*?]\\*\\'" ""
                                 crm-separator)
                                (car args))
                        (cdr args)))
                (advice-add #'completing-read-multiple :filter-args #'crm-indicator)

                ;; 不允许鼠标出现在minibuffer的提示中
                (setq minibuffer-prompt-properties
                      '(read-only t cursor-intangible t face minibuffer-prompt))
                (add-hook 'minibuffer-setup-hook #'cursor-intangible-mode)

                ;; 在emacs 28以后,非当前mode的指令都会被隐藏,vertico的指令也会隐藏
                (setq read-extended-command-predicate
                      #'command-completion-default-include-p)

                ;; minibuffer可循环
                (setq enable-recursive-minibuffers t)))

            ;; http://trey-jackson.blogspot.com/2010/04/emacs-tip-36-abort-minibuffer-when.html
            ;; 使用鼠标时关闭minibuffer
            (defun idiig/stop-using-minibuffer ()
              "kill the minibuffer"
              (when (and (>= (recursion-depth) 1) (active-minibuffer-window))
                (abort-recursive-edit)))
            (add-hook 'mouse-leave-buffer-hook 'idiig/stop-using-minibuffer)

            (use-package vertico
              :after consult
              :custom
              (vertico-count 9)
              (vertico-cycle t)
              :init
              (vertico-mode))
            (use-package orderless
              :after
              (consult)
              :init
              (defvar +orderless-dispatch-alist
              '((?% . char-fold-to-regexp)    ; %word% - 字符折叠匹配
                (?! . orderless-without-literal) ; !word! - 排除匹配
                (?`. orderless-initialism)    ; `word` - 首字母匹配
                (?= . orderless-literal)      ; =word= - 字面匹配
                (?~ . orderless-flex)))	  ; ~word~ - 弹性匹配
              :config
              (setq search-default-mode t)

              (defun +orderless--suffix-regexp ()
                (if (and (boundp 'consult--tofu-char) (boundp 'consult--tofu-range))
                    (format "[%c-%c]*$"
                            consult--tofu-char
                            (+ consult--tofu-char consult--tofu-range -1))
                  "$"))
              ;; Recognizes the following patterns:
              ;; * ~flex flex~
              ;; * =literal literal=
              ;; * %char-fold char-fold%
              ;; * `initialism initialism`
              ;; * !without-literal without-literal!
              ;; * .ext (file extension)
              ;; * regexp$ (regexp matching at end)
              (defun +orderless-dispatch (word _index _total)
                (cond
                 ;; Ensure that $ works with Consult commands, which add disambiguation suffixes
                 ((string-suffix-p "$" word)
                  `(orderless-regexp . ,(concat (substring word 0 -1) (+orderless--suffix-regexp))))
                 ;; File extensions
                 ((and (or minibuffer-completing-file-name
                           (derived-mode-p 'eshell-mode))
                       (string-match-p "\\`\\.." word))
                  `(orderless-regexp . ,(concat "\\." (substring word 1) (+orderless--suffix-regexp))))
                 ;; Ignore single !
                 ((equal "!" word) `(orderless-literal . ""))
                 ;; Prefix and suffix
                 ((if-let (x (assq (aref word 0) +orderless-dispatch-alist))
                      (cons (cdr x) (substring word 1))
                    (when-let (x (assq (aref word (1- (length word))) +orderless-dispatch-alist))
                      (cons (cdr x) (substring word 0 -1)))))))

              ;; Define orderless style with initialism by default ; add migemo feature for japanese
              (orderless-define-completion-style +orderless-with-initialism
                (orderless-matching-styles '(orderless-initialism
                                             orderless-literal
                                             orderless-regexp)))

              (setq completion-styles '(orderless basic)
                    completion-category-defaults nil
                    ;;; Enable partial-completion for files.
                    ;;; Either give orderless precedence or partial-completion.
                    ;;; Note that completion-category-overrides is not really an override,
                    ;;; but rather prepended to the default completion-styles.
                    ;; completion-category-overrides '((file (styles orderless partial-completion))) ;; orderless is tried first
                    completion-category-overrides '((file (styles partial-completion)) ;; partial-completion is tried first
                                                    (buffer (styles +orderless-with-initialism))
                                                    (consult-location (styles +orderless-with-initialism))
                                                    ;; enable initialism by default for symbols
                                                    (command (styles +orderless-with-initialism))
                                                    (variable (styles +orderless-with-initialism))
                                                    (symbol (styles +orderless-with-initialism)))
                    orderless-component-separator #'orderless-escapable-split-on-space ;; allow escaping space with backslash!
                    orderless-style-dispatchers '(+orderless-dispatch)))
            (use-package marginalia
              :after vertico
              ;; 只在minibuffer启用快捷键
              :bind (:map minibuffer-local-map ("M-A" . marginalia-cycle))
              :init
              (setq marginalia-align-offset 5)
              :config
              (marginalia-mode))
            (use-package consult
              :hook (after-init . (lambda () (require 'consult)))
              :bind (([remap M-x] . execute-extended-command)
                     ([remap goto-line] . consult-goto-line)
                     ([remap switch-to-buffer] . consult-buffer)
                     ([remap find-file] . find-file)
                 ([remap imenu] . consult-imenu)
                     ("C-c r" . consult-recent-file)
                     ("C-c y" . consult-yasnippet)
                     ("C-c f" . consult-find)
                     ("C-c s" . consult-line)
                     ("C-c o" . consult-file-externally)
                     ("C-c p f" . consult-ripgrep)
                     (:map minibuffer-local-map
                           ("C-c h" . consult-history)
                           ("C-s" . #'previous-history-element)))
              :init
              (add-to-list 'exec-path "${pkgs.fd}/bin")
              (add-to-list 'exec-path "${pkgs.ripgrep}/bin")
              (defun idiig/consult-buffer-region-or-symbol ()
                "consult-line当前字符或选中区域."
                (interactive)
                (let ((input (if (region-active-p)
                                 (buffer-substring-no-properties
                                  (region-beginning) (region-end))
                               (thing-at-point 'symbol t))))
                  (consult-line input)))
              (defun idiig/consult-project-region-or-symbol (&optional default-inputp)
                "consult-ripgrep 当前字符或选中区域."
                (interactive)
                (let ((input (if (region-active-p)
                                 (buffer-substring-no-properties
                                  (region-beginning) (region-end))
                               (thing-at-point 'symbol t))))
                  (consult-ripgrep default-inputp input)))
              :config
              (progn
                ;; (defvar my-consult-line-map
                ;;   (let ((map (make-sparse-keymap)))
                ;;     (define-key map "C-s" #'previous-history-element)
                ;;     map))
                ;; (consult-customize consult-line :keymap my-consult-line-map)
                ;; ;; 禁止自动显示consult文件的内容
                (setq consult-preview-key "C-v")
                ;; 应用 Orderless 的正则解析到 consult-grep/ripgrep/find
                (defun consult--orderless-regexp-compiler (input type &rest _config)
                  (setq input (orderless-pattern-compiler input))
                  (cons
                   (mapcar (lambda (r) (consult--convert-regexp r type)) input)
                   (lambda (str) (orderless--highlight input str))))
                ;; 表示的buffer种类
                (defcustom consult-buffer-sources
                  '(consult--source-hidden-buffer
                    consult--source-buffer
                    consult--source-file
                    consult--source-bookmark
                    consult--source-project-buffer
                    consult--source-project-file)
                  "Sources used by `consult-buffer'. See `consult--multi' for a description of the source values."
                  :type '(repeat symbol))
                ;; ?提示检索buffer类型;f<SPC>=file, p<SPC>=project, etc..
                (define-key consult-narrow-map
                        (vconcat consult-narrow-key "?") #'consult-narrow-help)))
            (use-package embark
              :after vertico
              :bind
              (("C-h B" . embark-bindings)  ;; alternative for `describe-bindings'
               (:map minibuffer-local-map
                     ("C-'" . embark-act)         ;; 对函数进行设置操作 
                     ("M-." . embark-dwim)        ;; 实施 
                     ("C-c C-e" . embark-export))) ;; occur 
              :init
              ;; Optionally replace the key help with a completing-read interface
              (setq prefix-help-command #'embark-prefix-help-command)
              :config
              (add-to-list 'display-buffer-alist
                           '("\\`\\*Embark Collect \\(Live\\|Completions\\)\\*"
                             nil
                             (window-parameters (mode-line-format . none)))))

            ;; embark-export弹出occur和grep mode的buffer
            (use-package embark-consult
              :ensure t
              :after (consult))
            (use-package vundo
              :defer t
              :commands
              (vundo)
              :bind
              ("C-x u" . vundo))
            (require 'ctrlf)
            (ctrlf-mode +1)
            (with-eval-after-load 'ctrlf

              ;; 定义 advice 函数
              (defun ctrlf-set-default-style-advice (style)
                "Advice function to set the default search style when changing styles.
            This ensures the selected style becomes the new default for future sessions."
                (setq ctrlf-default-search-style style))

              ;; 添加 advice
              (advice-add 'ctrlf-change-search-style :after #'ctrlf-set-default-style-advice))
            (use-package wgrep
              :config
              (setq wgrep-auto-save-buffer t)
              (setq wgrep-enable-key "e"))
            (use-package emacs
              :init
              ;; 启用自动括号配对
              (electric-pair-mode t)

              :config
              ;; 配置 electric-pair-mode 行为
              (setq electric-pair-preserve-balance nil)
              ;; 使用保守的抑制策略
              ;; https://www.reddit.com/r/emacs/comments/4xhxfw/how_to_tune_the_behavior_of_eletricpairmode/
              (setq electric-pair-inhibit-predicate 'electric-pair-conservative-inhibit)

              ;; 保存默认的配对括号设置,以便创建模式特定的本地设置
              (defconst idiig/default-electric-pairs electric-pair-pairs)

              ;; 为特定模式添加本地电子配对
              (defun idiig/add-local-electric-pairs (pairs)
                "为当前缓冲区添加本地电子配对括号。

                 参数:
                   PAIRS: 要添加的括号对列表

                 示例用法:
                   (add-hook 'jupyter-org-interaction-mode-hook
                             (lambda () (idiig/add-local-electric-pairs '((?$ . ?$)))))"
                (setq-local electric-pair-pairs (append idiig/default-electric-pairs pairs))
                (setq-local electric-pair-text-pairs electric-pair-pairs))

              ;; 禁止自动配对尖括号 <>
              (add-function :before-until electric-pair-inhibit-predicate
                            (lambda (c) (eq c ?<)))

              ;; 增强的括号匹配高亮——即使光标在括号内也能高亮匹配的括号
              (define-advice show-paren-function (:around (fn) fix-show-paren-function)
                "即使光标不直接位于括号上,也能高亮匹配的括号。"
                (cond ((looking-at-p "\\s(") (funcall fn))
                      (t (save-excursion
                           (ignore-errors (backward-up-list))
                           (funcall fn)))))

              ;; 启用括号匹配高亮
              (show-paren-mode t))
            (use-package puni
              :defer t
              :bind
              (:map puni-mode-map
                ([remap puni-kill-line] . idiig/puni-kill-line)
                ("C--" . idiig/puni-contract-region)
                ("C-=" . puni-expand-region))
              :init
              ;; The autoloads of Puni are set up so you can enable `puni-mode` or
              ;; `puni-global-mode` before `puni` is actually loaded. Only after you press
              ;; any key that calls Puni commands, it's loaded.
              (puni-global-mode)
              (add-hook 'term-mode-hook #'puni-disable-puni-mode)
              :config
              (defun idiig/puni-kill-line (&optional n)
                "Kill a line forward while keeping expressions balanced.
              If forward kill is not possible, try backward. If still nothing
              can be deleted, kill the balanced expression around point."
                (interactive "p")
                (let ((bounds (puni-bounds-of-list-around-point)))
                  (cond
                   ;; Case 1: No list bounds found, try deleting surrounding sexp
                   ((null bounds)
                    (when-let ((sexp-bounds (puni-bounds-of-sexp-around-point)))
                      (puni-delete-region (car sexp-bounds) (cdr sexp-bounds) 'kill)))

                   ;; Case 2: Point is at end of bounds, try backward kill
                   ((eq (point) (cdr bounds))
                    (puni-backward-kill-line))

                   ;; Case 3: Default forward kill
                   (t
                    (puni-kill-line n)))))
              (defun idiig/puni-contract-region (&optional arg)
                "如无选中则保持 negative-argument,如有选中则缩小范围"
                (interactive "p")
                (if (region-active-p)
                    (call-interactively #'puni-contract-region)
                  (negative-argument arg)))

            )
            ;; 添加 advice
            (with-eval-after-load 'puni
              (defun idiig/puni-expand-region-advice (orig-fun &rest args)
                "使用选中后的操作"

                (let* ((ev last-command-event)
                       (echo-keystrokes nil))
                  ;; 执行初始调整
                  (apply orig-fun args)

                  ;; 设置 transient map
                  (let ((delta (car args))) 
                (set-transient-map
                 (let ((map (make-sparse-keymap)))
                       ;; 持续扩大
                       (define-key map (kbd "=") 'puni-expand-region)
                       ;; 缩小范围
                       (define-key map (kbd "-") 'puni-contract-region)
                   ;; 其他操作
                   ;; 检索
                       (define-key map (kbd "/") 'idiig/consult-project-region-or-symbol)
                       (define-key map (kbd "b") 'idiig/consult-buffer-region-or-symbol)
                   ;; 加包围
                   (define-key map (kbd ")") 'puni-wrap-round)
                       (define-key map (kbd "]") 'puni-wrap-square)
                   (define-key map (kbd "}") 'puni-wrap-curly)
                   (define-key map (kbd ">") 'puni-wrap-angle)
                   map)
                 nil nil
                 "Use %k for further adjustment"))))
              (advice-add 'puni-expand-region :around #'idiig/puni-expand-region-advice))
            (defun idiig/backward-hungry-delete-advice (orig-fun &rest args)
              "Advice function to provide hungry delete functionality."
              (if (or (looking-back (rx (+ blank))) (bolp))
                  (let ((start (save-excursion (skip-chars-backward " \t\f\n\r\v") (point))))
                    (delete-region start (point)))
                (apply orig-fun args)))

            (defun idiig/apply-backward-hungry-delete-advice ()
              "Reapply the hungry delete advice to the current DEL key binding function."
              (let ((current-fun (key-binding (kbd "DEL"))))
                (advice-remove current-fun #'idiig/backward-hungry-delete-advice)    ; 移除旧的 advice
                (advice-add current-fun :around #'idiig/backward-hungry-delete-advice))) ; 应用新的 advice

            ;; 在 emacs-startup 时应用 advice
            (add-hook 'emacs-startup-hook #'idiig/apply-backward-hungry-delete-advice)

            ;; 如果你有其他 hook 如打开某种模式时,需要重新应用 advice,可添加对应 hook,例如:
            ;; (add-hook 'your-major-mode-hook #'idiig/reapply-backward-hungry-delete-advice)
            (defun idiig/forward-hungry-delete-advice (orig-fun &rest args)
              "Advice function to provide forward hungry delete functionality."
              (if (looking-at (rx (or (1+ blank) "\n")))
                  (let ((end (save-excursion
                               (skip-chars-forward " \t\f\v\n\r")
                               (point))))
                    (delete-region (point) end))
                (apply orig-fun args)))

            (defun idiig/apply-forward-hungry-delete-advice ()
              "Apply the forward hungry delete advice to the current forward delete key binding function."
              (let ((current-fun (key-binding (kbd "C-d"))))
                (advice-remove current-fun #'idiig/forward-hungry-delete-advice) ; 移除旧的 advice
                (advice-add current-fun :around #'idiig/forward-hungry-delete-advice))) ; 应用新的 advice

            ;; 在 emacs-startup 时应用 advice
            (add-hook 'emacs-startup-hook #'idiig/apply-forward-hungry-delete-advice)

            ;; 如果你有其他 hook 需要重新应用 advice,可添加对应 hook,例如:
            ;; (add-hook 'your-major-mode-hook #'idiig/apply-forward-hungry-delete-advice)
            (defun idiig/backward-kill-word-or-region-advice (orig-fun &rest args)
              "Enhance the C-w function to handle region more flexibly."
              (if (region-active-p)
                  ;; 当有选中区域时,使用传递的参数调用原始C-w功能(例如 `puni-kill-region`)
                  (apply orig-fun args)
                ;; 当没有选中区域时,执行删除单词操作
                (let ((backward-kill-word-fun (or (key-binding (kbd "M-<DEL>"))
                                                  (key-binding (kbd "S-<delete>"))
                                                  'backward-kill-word))) ; 默认删除单词函数
                  (if (fboundp backward-kill-word-fun)
                      (call-interactively backward-kill-word-fun) ; 交互式调用删除单词
                    (message "No word kill bound function found for M-<DEL> or S-<delete>")))))

            (defun idiig/apply-backward-kill-word-or-region-advice ()
              "Advice C-w to optionally kill region or word."
              ;; 通过 `key-binding` 得到当前与 C-w 绑定的函数
              (let ((current-fun (key-binding (kbd "C-w"))))
                (advice-remove current-fun #'idiig/backward-kill-word-or-region-advice)
                (advice-add current-fun :around #'idiig/backward-kill-word-or-region-advice)))

            ;; 在 emacs 启动时应用这个 advice
            (add-hook 'emacs-startup-hook #'idiig/apply-backward-kill-word-or-region-advice)
            (add-hook 'after-init-hook
                  (lambda ()
                    (let* ((screen-height (display-pixel-height))
                           (font-height (if (> screen-height 1200) 230 130))  ;; 根据屏幕高度调整
                           (minibuffer-font-height (- font-height 0))
                           (my-font "Sarasa Mono SC"))
                      (set-face-attribute 'default nil :family my-font :height font-height)
                      ;; 设置 mode-line 字体
                      (set-face-attribute 'mode-line nil :family my-font :height font-height)
                      (set-face-attribute 'mode-line-inactive nil :family my-font :height font-height)
                      ;; 设置 minibuffer 字体
                      (set-face-attribute 'minibuffer-prompt nil :family my-font :height minibuffer-font-height))))

            ;; 工具栏,菜单保持默认字体
            (set-face-attribute 'menu nil :inherit 'unspecified)
            (set-face-attribute 'tool-bar nil :inherit 'unspecified)
            (use-package ddskk
              :defer t
              :bind (("C-x j" . skk-mode))
              :config
              (setq skk-server-inhibit-startup-server nil)
              (setq skk-server-host "localhost")
              (setq skk-server-portnum 55100)
              (setq skk-share-private-jisyo t)

              ;; 候补显示设置
              (setq skk-show-inline t)
              (setq skk-show-tooltip t)
              (setq skk-show-candidates-always-pop-to-buffer t)
              (setq skk-henkan-show-candidates-rows 2)

              ;; 行为设置
              (setq skk-egg-like-newline t)
              (setq skk-delete-implies-kakutei nil)
              (setq skk-use-look t)
              (setq skk-auto-insert-paren t)
              (setq skk-henkan-strict-okuri-precedence t)

              ;; 片假名转换设置
              (setq skk-search-katakana 'jisx0201-kana)

              ;; 加载额外功能
              (require 'skk-hint)
              :hook
              (skk-load . (lambda ()
                            (require 'context-skk))))
            (require 'migemo)
            ;; cmigemo(default)
            (setq migemo-command "${pkgs.cmigemo}/bin/cmigemo")
            (setq migemo-options '("-q" "--emacs"))

            ;; Set your installed path
            (setq migemo-dictionary "${pkgs.cmigemo}/share/migemo/utf-8/migemo-dict")

            (setq migemo-user-dictionary nil)
            (setq migemo-regex-dictionary nil)
            (when (and migemo-command migemo-dictionary)
              (migemo-init)
              (message "Migemo initialized with dictionary: %s" migemo-dictionary))
            (with-eval-after-load 'migemo
              (with-eval-after-load 'ctrlf
                (add-to-list 'ctrlf-style-alist '(migemo-regexp . (:prompt "migemo-regexp"
                                                                       :translator migemo-search-pattern-get
                                                                       :case-fold ctrlf-no-uppercase-regexp-p)))))

            (with-eval-after-load 'orderless
              (defun orderless-migemo (component)
              (let ((pattern (migemo-get-pattern component)))
                (condition-case nil
                    (progn (string-match-p pattern "") pattern)
                  (invalid-regexp nil))))

              (add-to-list '+orderless-dispatch-alist '(?# . orderless-migemo)))
            (use-package pyim
              :diminish pyim-isearch-mode
              :commands
              (toggle-input-method)
              :custom
              (default-input-method "pyim")
              (pyim-dcache-directory (concat user-emacs-directory "pyim/dcache"))
              (pyim-default-scheme 'quanpin)
              (pyim-page-tooltip 'popup)
              (pyim-page-length 4))

            ;; 加载并启用基础词库
            (use-package pyim-basedict
              :after pyim
              :config
              (pyim-basedict-enable))
            (with-eval-after-load 'pyim
              (require 'pyim-cstring-utils)

              ;; C-return 把当前选中的位置转换为正则表达
              (define-key minibuffer-local-map (kbd "C-<return>") 'pyim-cregexp-convert-at-point)

              (defvar idiig/pyim-region-enabled nil
                "记录pyim区域功能是否启用的状态变量。")

              (defun idiig/toggle-pyim-region ()
                "切换pyim的单词移动功能。
            当启用时,会将forward-word和backward-word重映射为pyim的相应函数;
            当禁用时,会恢复原来的映射。"
                (interactive)
                (if idiig/pyim-region-enabled
                (progn
                  (idiig/disable-pyim-region)
                  (setq idiig/pyim-region-enabled nil)
                  (message "已禁用pyim区域功能"))
                  (progn
                (idiig/enable-pyim-region)
                (setq idiig/pyim-region-enabled t)
                (message "已启用pyim区域功能"))))

              (defun idiig/enable-pyim-region (&rest _)
                "启用pyim的单词移动建议。"
                (global-set-key [remap forward-word] 'pyim-forward-word)
                (global-set-key [remap backward-word] 'pyim-backward-word))

              (defun idiig/disable-pyim-region (&rest _)
                "禁用pyim的单词移动建议。"
                (global-unset-key [remap forward-word])
                (global-unset-key [remap backward-word]))

              ;; ;; 挂钩到 pyim 的启用/禁用钩子上
              ;; (advice-remove 'pyim-deactivate #'idiig/disable-pyim-region)
              ;; (advice-remove 'pyim-activate #'idiig/enable-pyim-region)
              ;; (advice-add 'pyim-deactivate :after #'idiig/disable-pyim-region)
              (advice-add 'pyim-activate :after #'idiig/enable-pyim-region))
            (with-eval-after-load 'ctrlf

              (defvar pyim-ctrlf-initialized nil
                "Flag to track if pyim data has been initialized for ctrlf.")

              (defvar pyim-ctrlf-cache (make-hash-table :test 'equal)
                "Cache for pyim-cregexp-build results.")

              (defconst pyim-ctrlf-vowels-with-mapping '("a" "e" "o")
                "Vowels that have direct Chinese character mappings.")

              (defconst pyim-ctrlf-double-consonants '("zh" "ch" "sh")
                "Double-letter consonants that should use regex-quote for exact matching.")

              (defun pyim-cregexp-build-lazy (str)
                "Lazy wrapper for pyim-cregexp-build with caching."
                (unless pyim-ctrlf-initialized
                  (message "Initializing pyim data for ctrlf...")
                  ;; 预缓存常用字符的结果
                  (call-interactively #'pyim-activate)
                  (call-interactively #'pyim-deactivate)

                  (dolist (vowel pyim-ctrlf-vowels-with-mapping)
                    (let ((result (pyim-cregexp-build vowel)))
                      (puthash vowel result pyim-ctrlf-cache)))
                  (setq pyim-ctrlf-initialized t)
                  (message "Pyim data initialized."))

                ;; 判断是否使用 regex-quote
                (if (or (and (= (length str) 1)
                             (not (member str pyim-ctrlf-vowels-with-mapping)))
                        (member str pyim-ctrlf-double-consonants))
                    (regexp-quote str)
                  ;; 使用缓存或计算新结果
                  (or (gethash str pyim-ctrlf-cache)
                      (let ((result (pyim-cregexp-build str)))
                        (puthash str result pyim-ctrlf-cache)
                        result))))

              (add-to-list 'ctrlf-style-alist
                           '(pinyin-regexp . (:prompt "pinyin-regexp"
                                                  :translator pyim-cregexp-build-lazy
                                                  :case-fold ctrlf-no-uppercase-regexp-p
                                                  :fallback (isearch-forward-regexp
                                                             . isearch-backward-regexp)))))
            ;; (with-eval-after-load 'orderless
            ;;   ;; 拼音检索字符串功能
            ;;   (defun zh-orderless-regexp (orig_func component)
            ;;     (call-interactively #'pyim-activate)
            ;;     (call-interactively #'pyim-deactivate)
            ;;     (let ((result (funcall orig_func component)))
            ;;       (pyim-cregexp-build result)))
            ;;   (advice-add 'orderless-regexp :around #'zh-orderless-regexp))

            (with-eval-after-load 'orderless

              (defvar pyim-orderless-initialized nil
                "Flag to track if pyim data has been initialized for orderless.")

              (defun orderless-pyim (component)
                (unless pyim-orderless-initialized
                  (message "Initializing pyim for orderless...")
                  ;; 预缓存常用字符的结果
                  (call-interactively #'pyim-activate)
                  (call-interactively #'pyim-deactivate)
                  (setq pyim-orderless-initialized t)
                  (message "Pyim data initialized."))

                (let ((pattern (pyim-cregexp-build component)))
                  (condition-case nil
                      (progn (string-match-p pattern "") pattern)
                (invalid-regexp nil))))

              (add-to-list '+orderless-dispatch-alist '(?@ . orderless-pyim)))
            (use-package magit
              :bind ("C-x g" . magit-status)
              :commands magit-status
              :init
              ;; 使用nix路径中的git
              (add-to-list 'exec-path "${pkgs.git}/bin"))
            (defvar idiig/writing-environment-list '("\\.org\\'"
                                                     "\\.md\\'"
                                                     "\\.qmd\\'"
                                                     "\\.rmd\\'"
                                                     "\\.typ\\'"
                                                     "\\.tex\\'"
                                                     "\\.bib\\'"
                                                     "\\.txt\\'"))
            (defun idiig/in-writing-environment-p ()
              "Check if current buffer file matches any pattern in idiig/writing-environment-list."
              (when (buffer-file-name)
                (cl-some (lambda (pattern)
                           (string-match-p pattern (buffer-file-name)))
                         idiig/writing-environment-list)))

            (add-hook 'find-file-hook
                      (lambda ()
                        (when (idiig/in-writing-environment-p)
                          (visual-line-mode 1))))

            (with-eval-after-load 'diminish
              (diminish 'visual-line-mode))
            (with-eval-after-load 'puni
              (defun idiig/backward-kill-word-or-region (&optional arg)
                (interactive "p")
                (if (region-active-p)
                (call-interactively #'puni-kill-active-region)
                  (backward-kill-word arg)))

              (global-set-key (kbd "C-w") 'idiig/backward-kill-word-or-region))
            (defun idiig/indent-buffer()
              (interactive)
              (indent-region (point-min) (point-max)))

            (defun idiig/indent-region-or-buffer()
              (interactive)
              (save-excursion
                (if (region-active-p)
                    (progn
                      (indent-region (region-beginning) (region-end)))
                  (progn
                    (idiig/indent-buffer)))))

            (global-set-key (kbd "C-M-\\") 'idiig/indent-region-or-buffer)
            (global-set-key (kbd "C-M-¥") 'idiig/indent-region-or-buffer)  ;; JIS keyboard
            (global-set-key [(shift return)] 'idiig/smart-open-line)
            (defun idiig/goto-match-paren (arg)
              "Go to the matching if on (){}[], similar to vi style of % "
              (interactive "p")
              ;; first, check for "outside of bracket" positions expected by forward-sexp, etc
              (cond ((looking-at "[\[\(\{]") (evil-jump-item))
                    ((looking-back "[\]\)\}]" 1) (evil-jump-item))
                    ;; now, try to succeed from inside of a bracket
                    ((looking-at "[\]\)\}]") (forward-char) (evil-jump-item))
                    ((looking-back "[\[\(\{]" 1) (backward-char) (evil-jump-item))
                    (t nil)))

            (bind-key* "M--" 'idiig/goto-match-paren)
            (defun idiig/insert-space-after-point ()
              (interactive)
              (save-excursion (insert " ")))

            (bind-key* "C-." 'idiig/insert-space-after-point)
            ;; TODO: 这里未来需要改成在每个语言的设定的节点push进来
            (defvar idiig/language-list
              '("emacs-lisp" "python" "ditaa" "plantuml" "shell" "nix"
                "R" "haskell" "latex" "css" "lisp" "jq" "makefile" "go")
              "支持的编程语言列表。")

            (defun idiig/run-prog-mode-hooks ()
              "Runs `prog-mode-hook'. 针对一些本该为编程语言又没自动加载prog mode的语言hook.
            如:(add-hook 'python-hook 'idiig/run-prog-mode-hooks)
            "
              (run-hooks 'prog-mode-hook))
            (defvar idiig/lsp-extra-paths nil
              "Emacs 侧已配置的 LSP 可执行目录清单。会被写入到项目的 .emacs-lsp-paths。")

            (defmacro idiig//setup-nix-lsp-bridge-server (language server-name executable-path &optional lib-path)
              "配置 Nix 环境下的 LSP 服务器。
            LANGUAGE 是语言名称,如 'python'。
            SERVER-NAME 是服务器名称,如 'basedpyright'。
            EXECUTABLE-PATH 是服务器可执行文件的路径。
            LIB-PATH 是可选的库路径,添加到 LD_LIBRARY_PATH。"
              `(with-eval-after-load 'lsp-bridge
                 ;; 设置 LSP 服务器
                 (setq ,(intern (format "lsp-bridge-%s-lsp-server" language)) ,server-name)

                 ;; 添加可执行文件路径到 exec-path
                 ,(when executable-path
                    `(progn
                   (add-to-list 'exec-path ,executable-path)
                   (add-to-list 'idiig/lsp-extra-paths ,executable-path)))

                 ;; 添加库路径到 LD_LIBRARY_PATH
                 ,(when lib-path
                    `(setenv "LD_LIBRARY_PATH" 
                             (concat ,lib-path ":" 
                                     (or (getenv "LD_LIBRARY_PATH") ""))))))
            (use-package lsp-bridge
              :defer t
              :diminish lsp-bridge-mode
              :bind
              (:map acm-mode-map
                    ("C-j" . acm-select-next)
                    ("C-k" . acm-select-prev))
              :custom
              (acm-enable-yas nil)   ; 补全不包括 Yasnippet
              (acm-enable-doc nil)   ; 不自动显示函数等文档
              (lsp-bridge-org-babel-lang-list idiig/language-list)  ; org支持的代码也使用桥
              (acm-enable-icon nil)  ; 不显示图标
              :hook 
              (prog-mode . (lambda ()
                         (lsp-bridge-mode)))
              :init
              ;; 这里是为了让语言服务器找到正确的版本的 libstdc++.so.6 库
              (setenv "LD_LIBRARY_PATH" 
                      (concat "${pkgs.stdenv.cc.cc.lib}/lib:" 
                              (or (getenv "LD_LIBRARY_PATH") ""))))
            (with-eval-after-load 'lsp-bridge
              (defun idiig/acm-prefer-lsp-all ()
                (when (bound-and-true-p lsp-bridge-mode)
                  ;; 让 search 后端慢一点再来(避免覆盖 LSP)
                  (when (boundp 'acm-backend-search-delay)
                    (setq-local acm-backend-search-delay 0.8))  ;; 你可调成 0.6~1.0

                  ;; 提高 LSP 优先级,降低 search 优先级(若有这些变量)
                  (when (boundp 'acm-backend-lsp-priority)
                    (setq-local acm-backend-lsp-priority 100))
                  (when (boundp 'acm-backend-search-priority)
                    (setq-local acm-backend-search-priority 0))

                  ;; 可选:减少噪声(若存在这些开关)
                  (when (boundp 'acm-enable-dabbrev)
                    (setq-local acm-enable-dabbrev nil))          ; 关闭 dabbrev 后端
                  (when (boundp 'acm-backend-search-candidates-min-length)
                    (setq-local acm-backend-search-candidates-min-length 3)))) ; 至少 3 字符再搜

              (add-hook 'lsp-bridge-mode-hook #'idiig/acm-prefer-lsp-all))
            (use-package treesit-auto
              :custom
              (treesit-auto-install 'prompt)   ; 设置安装 tree-sitter 语法时提示用户确认
              :hook
              (prog-mode . treesit-auto-mode)    ; 在所有编程模式下自动启用 treesit-auto-mode
              :config
              (treesit-auto-add-to-auto-mode-alist 'all))  ; 将所有已知的 tree-sitter 模式添加到自动模式列表中
            ;; (defvar idiig/snippet-dir (concat user-emacs-directory "snippets"))
            (use-package yasnippet
              :defer t
              :diminish yas-minor-mode
              :hook
              (prog-mode . yas-minor-mode)
              :init
              ;; (setq yas-snippet-dirs <path/to/snippets>)
              ;; (push idiig/snippet-dir yas-snippet-dirs)
              :config
              (yas-reload-all))
            (use-package consult-yasnippet
              :after
              (consult
               yas-minor-mode))

            (use-package direnv
              :defer t
              :init
              (add-to-list 'exec-path "${pkgs.direnv}/bin")
              :config
              (direnv-mode))
            (require 'cl-lib)

            (defun idiig/project-root ()
              "返回当前 buffer 对应的项目根(优先含 .envrc,其次 .git)。"
              (or (locate-dominating-file default-directory ".envrc")
                  (locate-dominating-file default-directory ".git")
                  default-directory))

            (defun idiig/write-emacs-lsp-paths ()
              "将 `idiig/lsp-extra-paths` 写入项目根的 .emacs-lsp-paths。"
              (interactive)
              (when-let* ((root (idiig/project-root))
                          (file (expand-file-name ".emacs-lsp-paths" root)))
                (let* ((dirs (->> idiig/lsp-extra-paths
                                  (seq-filter #'file-directory-p)
                                  (delete-dups))))
                  (when dirs
                    (with-temp-file file
                      (dolist (p dirs) (insert p "\n")))))))

            ;; lsp-bridge 项目根识别(避免偶发 no views)
            ;; direnv 集成:allow/refresh 前写清单;完成后自动重启 lsp-bridge
            (with-eval-after-load 'direnv
              ;; before:生成/更新 .emacs-lsp-paths,供 .envrc 读取
              (advice-add 'direnv-allow :before (lambda (&rest _) (idiig/write-emacs-lsp-paths)))
              (when (fboundp 'direnv-update-environment)
                (advice-add 'direnv-update-environment :before
                            (lambda (&rest _) (idiig/write-emacs-lsp-paths))))

              ;; after:环境就绪后,如有需要自动重启 lsp-bridge
              (defun idiig/direnv--restart-lsp-bridge (&rest _)
                (when (and (featurep 'lsp-bridge)
                           (fboundp 'lsp-bridge-restart-process)
                           (cl-some (lambda (buf)
                                      (with-current-buffer buf
                                        (bound-and-true-p lsp-bridge-mode)))
                                    (buffer-list)))
                  (lsp-bridge-restart-process)))
              (advice-add 'direnv-allow :after #'idiig/direnv--restart-lsp-bridge)
              (when (fboundp 'direnv-update-environment)
                (advice-add 'direnv-update-environment :after #'idiig/direnv--restart-lsp-bridge)))
            ;; For `eat-eshell-mode'.
            (add-hook 'eshell-load-hook #'eat-eshell-mode)

            ;; For `eat-eshell-visual-command-mode'.
            (add-hook 'eshell-load-hook #'eat-eshell-visual-command-mode)
            (idiig//setup-nix-lsp-bridge-server 
             "nix" 
             "nixd" 
             "${pkgs.nixd}/bin" 
             nil)
            (use-package slime
              :init
              (setq inferior-lisp-program
                (or (executable-find "sbcl")
                    "${pkgs.sbcl}/bin/sbcl"))
              :config
              (slime-setup '(slime-fancy)))
            (add-hook 'eval-expression-minibuffer-setup 'idiig/run-prog-mode-hooks)
            (idiig//setup-nix-lsp-bridge-server 
             "clojure" 				; language name
             "clojure-lsp" 				; lsp name
             "${pkgs.clojure-lsp}/bin"		; dependency nixpkg path
             nil)					; other dependencies
            (idiig//setup-nix-lsp-bridge-server 
             "python" 
             "basedpyright" 
             "${pkgs.basedpyright}/bin" 
             "${pkgs.stdenv.cc.cc.lib}/lib")
            (idiig//setup-nix-lsp-bridge-server 
             "haskell"
             "hls" 
             "${pkgs.haskell-language-server}/bin" 
             nil)
            (idiig//setup-nix-lsp-bridge-server 
             "go" 
             "gopls" 
             "${pkgs.gopls}/bin" 
             nil)
            (defun idiig/go-prefer-lsp ()
              (when (derived-mode-p 'go-mode 'go-ts-mode)
                ;; 关闭文件内/跨缓冲词搜索后端(如果你的版本有这些开关)
                (when (boundp 'acm-enable-search-file-words)
                  (setq-local acm-enable-search-file-words nil))
                (when (boundp 'acm-enable-dabbrev)
                  (setq-local acm-enable-dabbrev nil))
                ;; 把搜索词的延迟调大,避免覆盖(若有这个变量)
                (when (boundp 'acm-backend-search-delay)
                  (setq-local acm-backend-search-delay 0.8))
                ;; LSP 候选最短前缀更短一些(若有)
                (when (boundp 'acm-backend-lsp-candidate-min-length)
                  (setq-local acm-backend-lsp-candidate-min-length 0))))
            (add-hook 'go-mode-hook #'idiig/go-prefer-lsp)
            (add-hook 'go-ts-mode-hook #'idiig/go-prefer-lsp)
            (idiig//setup-nix-lsp-bridge-server 
             "bash" 				; language name
             "bash-language-server" 		; lsp name
             "${pkgs.bash-language-server}/bin" 	; dependency nixpkg path
             nil)					; other dependencies
            ;; (setq shell-command-switch "-ic")
            (setq-default explicit-shell-file-name "${pkgs.bashInteractive
            }/bin/bash")
            (setq shell-file-name "${pkgs.bashInteractive
            }/bin/bash")
            (idiig//setup-nix-lsp-bridge-server 
             "tex" 
             "texlab" 
             "${pkgs.texlab}/bin" 
             nil)
            (add-hook 'TeX-mode-hook 'idiig/run-prog-mode-hooks)
            (use-package auctex
              :defer t)
            (idiig//setup-nix-lsp-bridge-server 
             "json" 
             "vscode-json-language-server" 
             "${pkgs.vscode-langservers-extracted}/bin" 
             nil)
            (use-package jsonian
              :after so-long
              :custom
              (jsonian-no-so-long-mode))
            (add-to-list 'exec-path "${pkgs.plantuml}/bin")
            (with-eval-after-load 'org
              (setq org-plantuml-jar-path "${pkgs.plantuml}/lib/plantuml.jar")
              (setq org-plantuml-executable-path "${pkgs.plantuml}/bin/plantuml")
              (setq org-plantuml-exec-mode 'plantuml))
            (add-to-list 'exec-path "${pkgs.graphviz}/bin")
            (add-hook 'org-mode-hook 'idiig/run-prog-mode-hooks)
            (defun idiig/load-org-babel-languages ()
              "根据 `idiig/language-list` 启用 `org-babel` 语言。"
              (let ((languages '()))
                (dolist (lang idiig/language-list)
                  (push (cons (intern lang) t) languages)) ;; 将字符串转换为符号
                (org-babel-do-load-languages 'org-babel-load-languages languages)))

            (defun idiig/set-org-babel-language-commands ()
              "根据 `idiig/language-list` 甚至语言的命令。"
              (dolist (lang idiig/language-list)
                (let ((var-name (intern (format "org-babel-%s-command" lang))))
                  (when (boundp var-name)
                (set var-name (executable-find lang))))))

            (add-hook 'org-mode-hook #'idiig/load-org-babel-languages)
            (add-hook 'org-mode-hook #'idiig/set-org-babel-language-commands)

            ;; 特殊
            (setq org-babel-shell-command (executable-find "bash"))
            (with-eval-after-load 'org
              (defun idiig/org-insert-structure-template-src-advice (orig-fun type)
                "Advice for org-insert-structure-template to handle src blocks."
                (if (string= type "src")  ; 判断条件为 "src"
                (let ((selected-type (ido-completing-read "Source code type: " idiig/language-list)))
                  (funcall orig-fun (format "src %s" selected-type)))
                  (funcall orig-fun type)))

              (advice-add 'org-insert-structure-template :around #'idiig/org-insert-structure-template-src-advice))
            (with-eval-after-load 'org
              (setq org-startup-with-inline-images t) ; 启动时显示图片
              (setq org-startup-with-latex-preview t) ; 启动时显示 LaTeX 公式
              (add-hook 'org-mode-hook
                        (lambda ()
                          (org-overview)		; 显示所有顶层节点
                      (org-show-entry)		; 显示当前节点内容
                          (org-show-children))))	; 显示所有子节点但不展开
            (with-eval-after-load 'org
              (setq org-support-shift-select 2))
            (with-eval-after-load 'org
              (setq org-display-remote-inline-images t))
            (with-eval-after-load 'org
              (setq org-export-allow-bind-keywords t))
            (with-eval-after-load 'org
             ;; Edit settings
             (setq org-auto-align-tags nil		; 禁用标签自动对齐功能
                   org-tags-column 0		; 标签紧贴标题文本,不右对齐
                   org-catch-invisible-edits 'show-and-error  ; 编辑折叠内容时显示并报错提醒
                   org-special-ctrl-a/e t ; 增强 C-a/C-e,先跳到内容开始/结束,再跳到行首/尾
                   org-insert-heading-respect-content t ; 插入标题时考虑内容结构,在内容后插入

                   ;; Org styling, hide markup etc.
                   org-hide-emphasis-markers t ; 隐藏强调标记符号 (*粗体* 显示为 粗体)
                   org-pretty-entities t))	  ; 美化显示实体字符 (\alpha 显示为 α)
            (defun idiig/org-mode-face-settings ()
              "Set custom face attributes for Org mode headings in current buffer only."

              (auto-fill-mode 0)			; Disable auto-fill mode
              (require 'org-indent)			; Ensure org-indent is loaded
              (org-indent-mode)			; Enable org-indent mode
              (variable-pitch-mode 1)		; Enable variable-pitch mode
              (visual-line-mode 1)			; Enable visual-line mode for soft wrapping

              (defvar idiig/fixed-width-font "Sarasa Mono J"
                "The font to use for monospaced (fixed width) text.")

              (defvar idiig/variable-width-font "Sarasa Gothic J"
                "The font to use for variable-pitch (document) text.")

              (set-face-attribute 'default nil
                              :font idiig/fixed-width-font
                              :weight 'regular
                              :height 160)
              (set-face-attribute 'fixed-pitch nil
                              :font idiig/fixed-width-font
                              :weight 'regular
                              :height 170)
              (set-face-attribute 'variable-pitch nil
                              :font idiig/variable-width-font
                              :weight 'regular
                              :height 1.3)
              (buffer-face-set `(:family ,idiig/fixed-width-font
                                     :height 1.1)) ; Set buffer face

              (setq-local line-spacing 0.3)		; Set line spacing

              (let ((faces '((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))))
                (dolist (face faces)
                  `(face-remap-add-relative (car face)
                                       :family ,idiig/variable-width-font
                                       :weight 'regular
                                       :height (cdr face))))

              ;; Make sure certain org faces use the fixed-pitch face when variable-pitch-mode is on
              (set-face-attribute 'org-block nil :foreground nil :inherit 'fixed-pitch)
              (set-face-attribute 'org-table nil :inherit 'fixed-pitch)
              (set-face-attribute 'org-formula nil :inherit 'fixed-pitch)
              (set-face-attribute 'org-code nil :inherit '(shadow fixed-pitch))
              (set-face-attribute 'org-verbatim nil :inherit '(shadow fixed-pitch))
              (set-face-attribute 'org-special-keyword nil :inherit '(font-lock-comment-face fixed-pitch))
              (set-face-attribute 'org-meta-line nil :inherit '(font-lock-comment-face fixed-pitch))
              (set-face-attribute 'org-checkbox nil :inherit 'fixed-pitch)

              ;; Make the document title a bit bigger
              (set-face-attribute 'org-document-title nil :font idiig/variable-width-font :weight 'bold :height 1.3)

              (with-eval-after-load 'diminish
                (diminish 'org-indent-mode)
                (diminish 'buffer-face-mode)))

            (add-hook 'org-mode-hook 'idiig/org-mode-face-settings)
            (with-eval-after-load 'org
              (add-to-list
               'org-preview-latex-process-alist
               '(idiig-dvisvgm
                 :programs ("${pkgs.texliveMedium}/bin/latex" "${pkgs.texliveMedium}/bin/dvisvgm")
                 :description "latex -> dvi -> svg (nix-store)"
                 :message "use latex and dvisvgm from nix-store."
                 :image-input-type "dvi"
                 :image-output-type "svg"
                 :image-size-adjust (0.8 . 1.0)
                 :latex-compiler ("${pkgs.texliveMedium}/bin/latex -interaction nonstopmode -output-directory %o %f")
                 :image-converter ("${pkgs.texliveMedium}/bin/dvisvgm %f --no-fonts --exact-bbox --scale=%S --output=%O")))

              (setq org-latex-create-formula-image-program 'idiig-dvisvgm))
            (use-package org-bullets
              :after org
              :hook (org-mode . org-bullets-mode)
              :custom
              (org-bullets-bullet-list '("◉" "○" "●" "○" "●" "○" "●")))

            (font-lock-add-keywords 'org-mode
                                    '(("^ *\\([-]\\) "
                                       (0 (prog1 () (compose-region (match-beginning 1) (match-end 1) "•"))))))

            (with-eval-after-load 'org
              (setq org-ellipsis " ▾"
                    org-hide-emphasis-markers t))
            (with-eval-after-load 'org
              (setq org-cite-export-processors
                  '((latex biblatex)
                    (html csl)
                    (odt  csl)
                    (t    biblatex))))
            (with-eval-after-load 'oc
              (require 'oc-basic)
              (require 'subr-x)

              ;; 样式选择和参数输入
              (defun my/oc--ask-style-and-affixes ()
                "询问 citation 样式和附加参数。"
                (let* ((style-help "
            Style examples:
              cite/a/cf → Citeauthor – Aa, Bb, and Cc
              cite/a/c  → Citeauthor – Aa et al.
              cite/na   → citeyear – 2022
              cite/na/b → citeyearpar – (2022)
              cite/t/c  → Citet – Aa et al. (2022)
              cite/t/cf → citep* – Aa, Bb, and Cc (2022)
              cite/bc   → Cite – Aa et al. 2022
              cite/cf   → Citep – (Aa et al. 2022)")
                       (presets '(("cite/a/cf" . ("a"  . "cf"))
                                  ("cite/a/c"  . ("a"  . "c"))
                                  ("cite/na"   . ("na" . ""))
                                  ("cite/na/b" . ("na" . "b"))
                                  ("cite/t/c"  . ("t"  . "c"))
                                  ("cite/t/cf" . ("t"  . "cf"))
                                  ("cite/bc"   . ("bc" . ""))
                                  ("cite/cf"   . ("cf" . ""))))
                       (choice (completing-read
                                "Citation style (? for help): "
                                (append (mapcar #'car presets) '("custom" "?"))
                                nil t nil nil "cite/cf"))
                       (l "") (b ""))

                  ;; 显示帮助
                  (when (string= choice "?")
                    (message "%s" style-help)
                    (sit-for 3)
                    (setq choice (completing-read "Citation style: "
                                                  (append (mapcar #'car presets) '("custom"))
                                                  nil t nil nil "cite/cf")))

                  ;; 获取样式
                  (if (not (string= choice "custom"))
                      (let ((p (cdr (assoc choice presets))))
                        (setq l (car p) b (cdr p)))
                    (setq l (completing-read "Style (l): " '("a" "na" "t" "bc" "c" "cf" "") nil t ""))
                    (setq b (completing-read "Variant (b): " '("b" "c" "cf" "") nil t "")))

                  ;; 构建完整样式和参数
                  (let* ((style (string-join (seq-filter (lambda (x) (and x (not (string-empty-p x))))
                                                         (list l b))
                                             "/"))
                         (prefix  (read-string "Prefix (optional): "))
                         (locator (read-string "Locator (optional): "))
                         (suffix  (read-string "Suffix (optional): ")))
                    (list style prefix locator suffix))))

              ;; 修改插入点的 citation
              (defun my/oc--rewrite-cite-at-point (style prefix locator suffix)
                "在当前位置修改 citation 的样式和参数。"
                (save-excursion
                  (let* ((ctx (org-element-context))
                         (cite (if (eq (org-element-type ctx) 'citation)
                                   ctx
                                 (when (re-search-backward "\\[cite\\>" (max (point-min) (- (point) 1000)) t)
                                   (org-with-point-at (match-beginning 0)
                                     (org-element-citation-parser))))))
                    (when cite
                      (let* ((beg (org-element-begin cite))
                             (end (org-element-end   cite))
                             (has-style (save-excursion (goto-char beg) (looking-at-p "\\[cite/")))
                             (has-colon (save-excursion
                                          (goto-char beg)
                                          (re-search-forward "\\[cite\\(?:/[^:]]+\\)?\\(:\\)" end t))))

                        ;; 添加样式
                        (when (and (not has-style) (not (string-empty-p style)))
                          (goto-char (+ beg 5))
                          (insert "/" style)
                          (setq end (+ end (length style) 1)))

                        ;; 添加前缀
                        (when (not (string-empty-p prefix))
                          (unless has-colon
                            (goto-char beg)
                            (re-search-forward "\\[cite\\(?:/[^:]]+\\)?" end t)
                            (insert ":")
                            (setq end (1+ end)))
                          (goto-char beg)
                          (re-search-forward ":" end t)
                          (insert prefix " ")
                          (setq end (+ end (length prefix) 1)))

                        ;; 添加定位符
                        (when (not (string-empty-p locator))
                          (goto-char beg)
                          (when (re-search-forward "@[^; \t\n]+" end t)
                            (insert " " locator)
                            (setq end (+ end (length locator) 1))))

                        ;; 添加后缀
                        (when (not (string-empty-p suffix))
                          (goto-char (1- end))
                          (insert " " suffix)))))))

              ;; 主 advice:插入后弹出二次对话
              (defun my/oc-insert-then-ask (orig-fn arg)
                "插入引用后询问样式和参数。"
                (let ((ret (funcall orig-fn arg)))
                  (run-at-time
                   0 nil
                   (lambda ()
                     (condition-case err
                         (pcase-let ((`(,style ,prefix ,locator ,suffix)
                                      (my/oc--ask-style-and-affixes)))
                           (my/oc--rewrite-cite-at-point style prefix locator suffix))
                       (quit  (message "Citation style canceled"))
                       (error (message "Error setting citation style: %s" (error-message-string err))))))
                  ret))

              ;; 挂载 advice
              (advice-add 'org-cite-insert :around #'my/oc-insert-then-ask))
            (use-package copilot
              :hook (prog-mode . copilot-mode)
              :config
              (define-key copilot-completion-map (kbd "<tab>") 'copilot-accept-completion)
              (add-to-list 'copilot-indentation-alist '(prog-mode 2))
              (add-to-list 'copilot-indentation-alist '(org-mode 2))
              (add-to-list 'copilot-indentation-alist '(text-mode 2))
              (add-to-list 'copilot-indentation-alist '(lisp-mode 2))
              (add-to-list 'copilot-indentation-alist '(emacs-lisp-mode 2))

              (setq copilot-max-char 99999999))
            (add-hook 'org-mode-hook
                      (lambda ()
                        (when (string-match-p "\\.ai\\.org\\'" (buffer-file-name))
                          (gptel-mode 1))))
            (add-to-list 'exec-path "${pkgs.aider-chat}/bin")
            (defvar idiig/supported-providers '("openai" "anthropic" "google")
              "List of supported AI providers for Aider.")

            (defun idiig/provider-env-var (provider)
              "Return the environment variable name for the given PROVIDER.
            Uppercase the provider name and append '_API_KEY'."
              (let ((provider-lower (downcase provider)))
                (if (member provider-lower idiig/supported-providers)
                    (concat (upcase provider-lower) "_API_KEY")
                  (error "Unsupported provider: %s" provider))))

            (defun idiig/api-path (provider dir)
              "Return the file path for the API key of PROVIDER in directory DIR."
              (expand-file-name provider dir))

            (defun idiig/read-file-contents (file-path)
              "Return the contents of FILE-PATH as a string, with error handling."
              (condition-case err
                  (if (file-exists-p file-path)
                      (string-trim (with-temp-buffer
                                     (insert-file-contents file-path)
                                     (buffer-string)))
                    (error "File does not exist: %s" file-path))
                (error
                 (message "Error reading file %s: %s" file-path (error-message-string err))
                 nil)))

            (defun idiig/setup-single-provider (provider api-dir)
              "Set up API key for a single PROVIDER from API-DIR.
            Return t on success, nil on failure."
              (let* ((provider-env (idiig/provider-env-var provider))
                     (provider-path (idiig/api-path provider api-dir))
                     (api-key (idiig/read-file-contents provider-path)))
                (if api-key
                    (progn
                      (setenv provider-env api-key)
                      (message "Set %s from %s" provider-env provider-path)
                      t)
                  (progn
                    (message "Failed to read API key for %s from %s" provider provider-path)
                    nil))))

            (defun idiig/get-default-api-dir ()
              "Get the default API directory.
            Check if 'api-key' or 'api-keys' folder exists in current directory.
            Return the path if found, otherwise return current directory."
              (let ((current-dir default-directory)
                    (possible-dirs '("api-key" "api-keys")))
                (or (seq-find (lambda (dir)
                                (let ((full-path (expand-file-name dir current-dir)))
                                  (and (file-directory-p full-path) full-path)))
                              possible-dirs)
                    ;; If none found, query user for directory using `read-directory-name`
                (read-directory-name "Select API keys directory: " current-dir nil t))))

            (defun idiig/setup-api-keys (&optional api-dir)
              "Set up API keys for Aider from directory containing API files.
            If API-DIR is provided, use it directly. Otherwise, check if current
            folder has 'api-key' or 'api-keys' folder and use it as default.
            Interactively prompts for the directory with smart default."
              (interactive)
              (let* ((default-dir (idiig/get-default-api-dir))
                     (prompt (if (string= default-dir default-directory)
                                 "Select API keys directory: "
                               (format "Select API keys directory (default: %s): " 
                                       (file-name-nondirectory (directory-file-name default-dir)))))
                     (selected-dir (if (called-interactively-p 'any)
                                       (read-directory-name prompt default-dir)
                                     (or api-dir default-dir)))
                     (results (mapcar (lambda (provider)
                                        (idiig/setup-single-provider provider selected-dir))
                                      idiig/supported-providers))
                     (success-count (length (seq-filter #'identity results)))
                     (total-count (length idiig/supported-providers)))

                ;; Provide comprehensive feedback
                (if (= success-count total-count)
                    (message "Successfully set all %d API keys from directory: %s" 
                             total-count selected-dir)
                  (message "Set %d of %d API keys from directory: %s (check messages for details)"
                           success-count total-count selected-dir))

                ;; Return success status for programmatic use
                (= success-count total-count)))
            (use-package aidermacs
              :bind (("C-c a" . aidermacs-transient-menu))
              :config
              (idiig/setup-api-keys)
              :custom
              (aidermacs-default-chat-mode 'architect)
              (aidermacs-default-model "sonnet"))
            ;; Add to your init.el
            (use-package claude-code
              :init
              (add-to-list 'exec-path "${pkgs.claude-code}/bin")
              :bind (("C-c c" . claude-code-transient)))
            (use-package meow
              :init
              ;; https://github.com/meow-edit/meow/blob/master/KEYBINDING_QWERTY.org
              (require 'meow)
              (defun meow-setup ()
                (setq meow-cheatsheet-layout meow-cheatsheet-layout-qwerty)
                (meow-motion-define-key
                 '("j" . meow-next)
                 '("k" . meow-prev)
                 '("<escape>" . ignore))
                (meow-leader-define-key
                 ;; Use SPC (0-9) for digit arguments.
                 '("1" . meow-digit-argument)
                 '("2" . meow-digit-argument)
                 '("3" . meow-digit-argument)
                 '("4" . meow-digit-argument)
                 '("5" . meow-digit-argument)
                 '("6" . meow-digit-argument)
                 '("7" . meow-digit-argument)
                 '("8" . meow-digit-argument)
                 '("9" . meow-digit-argument)
                 '("0" . meow-digit-argument)
                 '("/" . meow-keypad-describe-key)
                 '("?" . meow-cheatsheet))
                (meow-normal-define-key
                 '("0" . meow-expand-0)
                 '("9" . meow-expand-9)
                 '("8" . meow-expand-8)
                 '("7" . meow-expand-7)
                 '("6" . meow-expand-6)
                 '("5" . meow-expand-5)
                 '("4" . meow-expand-4)
                 '("3" . meow-expand-3)
                 '("2" . meow-expand-2)
                 '("1" . meow-expand-1)
                 '("-" . negative-argument)
                 '(";" . meow-reverse)
                 '("," . meow-inner-of-thing)
                 '("." . meow-bounds-of-thing)
                 '("[" . meow-beginning-of-thing)
                 '("]" . meow-end-of-thing)
                 '("a" . meow-append)
                 '("A" . meow-open-below)
                 '("b" . meow-back-word)
                 '("B" . meow-back-symbol)
                 '("c" . meow-change)
                 '("d" . meow-delete)
                 '("D" . meow-backward-delete)
                 '("e" . meow-next-word)
                 '("E" . meow-next-symbol)
                 '("f" . meow-find)
                 '("g" . meow-cancel-selection)
                 '("G" . meow-grab)
                 '("h" . meow-left)
                 '("H" . meow-left-expand)
                 '("i" . meow-insert)
                 '("I" . meow-open-above)
                 '("j" . meow-next)
                 '("J" . meow-next-expand)
                 '("k" . meow-prev)
                 '("K" . meow-prev-expand)
                 '("l" . meow-right)
                 '("L" . meow-right-expand)
                 '("m" . meow-join)
                 '("n" . meow-search)
                 '("o" . meow-block)
                 '("O" . meow-to-block)
                 '("p" . meow-yank)
                 '("q" . meow-quit)
                 '("Q" . meow-goto-line)
                 '("r" . meow-replace)
                 '("R" . meow-swap-grab)
                 '("s" . meow-kill)
                 '("t" . meow-till)
                 '("u" . meow-undo)
                 '("U" . meow-undo-in-selection)
                 '("v" . meow-visit)
                 '("w" . meow-mark-word)
                 '("W" . meow-mark-symbol)
                 '("x" . meow-line)
                 '("X" . meow-goto-line)
                 '("y" . meow-save)
                 '("Y" . meow-sync-grab)
                 '("z" . meow-pop-selection)
                 '("'" . repeat)
                 '("<escape>" . ignore)))
              (meow-setup)
              :config
              (meow-global-mode 1))
            (require 'meow-tree-sitter)
            (meow-tree-sitter-register-defaults)  
            (defvar-local the-late-input-method nil)
            (add-hook 'meow-insert-enter-hook
                  (lambda ()
                    (activate-input-method the-late-input-method)))
            (add-hook 'meow-insert-exit-hook
                  (lambda ()
                    (setq the-late-input-method current-input-method)
                    (deactivate-input-method)))
            (require 'eaf)
            (require 'eaf-browser)
            (require 'eaf-pdf-viewer)
            (add-to-list 'exec-path "${pkgs.wmctrl}/bin")
            (setq eaf-webengine-default-zoom 2.0
                    eaf-browse-blank-page-url "https://www.kagi.com"
                    eaf-browser-auto-import-chrome-cookies nil   ; 非自动 cookies
                    eaf-browser-enable-autofill t                ; 自动填充密码
                    eaf-browser-enable-tampermonkey t)	     ; 使用油猴
            '';

                      # early-init 配置文件
                      emacsEarlyInitConfig = pkgs.writeText "early-init.el" ''
            ;; 增加 GC 阈值,加快启动
            (setq gc-cons-threshold 402653184 gc-cons-percentage 0.6)

            ;; 启动完成后恢复正常 GC 设定
            (add-hook 'emacs-startup-hook
                  (lambda ()
                    (setq gc-cons-threshold 10485760
                          gc-cons-percentage 0.1)))

            ;; 禁用bidi,加速大文件
            (setq-default bidi-display-reordering nil)
            (setq bidi-inhibit-bpa t
                  long-line-threshold 1000
                  large-hscroll-threshold 1000
                  syntax-wholeline-max 1000)
            '';

          # 首先定义你的基础 Emacs
          emacs = pkgs.emacs30-gtk3;

          # 定义覆盖函数
          overrides = final: prev: mkPackages pkgs final;

          # 创建扩展的包集合并选择包
          emacsWithPackages = ((pkgs.emacsPackagesFor emacs).overrideScope overrides).withPackages (epkgs: with epkgs; [
            use-package
              diminish
            so-long
            spacious-padding
              writeroom-mode
            mwim
            unfill
            vertico
              orderless
              marginalia
              embark
              consult
              embark-consult
            vundo
            ctrlf
            wgrep
            puni
            ddskk
            migemo
            pyim
              pyim-basedict
            magit
            (lsp-bridge.override {
              # 指定使用 Python 3.11 而不是 3.12
              python3 = pkgs.python311;
            })
              markdown-mode
              yasnippet
            # treesit  # 目前 treesit 已经内置
            treesit-auto
            # yasnippet
            yasnippet-snippets
              consult-yasnippet

            direnv
            eat
            nix-mode
            slime
              geiser                        # for scheme
            haskell-mode
            go-mode
            jq-mode
            auctex
              auctex-latexmk
            jsonian
              json-mode
            plantuml-mode
            ob-nix
              ob-go
            org-bullets
            citeproc
            org-present
            copilot
            gptel
            aidermacs
            claude-code
            meow
              meow-tree-sitter
            (eaf.withApplications [ eaf-browser eaf-pdf-viewer ])
          ]);

              in {
                      packages.default = pkgs.writeShellScriptBin "script" ''
              #!/usr/bin/env bash
              set -e

              # 导出配置到 nix-emacs
              EMACS_DIR="$HOME/nix-emacs"
              mkdir -p "$EMACS_DIR"
              ${pkgs.rsync}/bin/rsync ${emacsConfig} "$EMACS_DIR/init.el"
              ${pkgs.rsync}/bin/rsync ${emacsEarlyInitConfig} "$EMACS_DIR/early-init.el"

              # 路径
              if [ "$(uname)" = "Darwin" ]; then
                  # macOS
                  mkdir -p "$HOME/Library/Fonts/"
                  ${pkgs.rsync}/bin/rsync -av ${pkgs.sarasa-gothic}/share/fonts/truetype/ "$HOME/Library/Fonts/"
              else
                  # Assume Linux
                  mkdir -p "$HOME/.local/share/fonts/truetype/"
                  ${pkgs.rsync}/bin/rsync -av ${pkgs.sarasa-gothic}/share/fonts/truetype/ "$HOME/.local/share/fonts/sarasa-gothic/"
                  fc-cache -f -v ~/.local/share/fonts/
              fi

              # 更新 Emacs 路径(兼容 macOS 和 Linux)
        if sed --version 2>/dev/null | grep "(GNU sed)"; then
                      sed -i '/^alias ne=/d' "$HOME/.bashrc"
              else
                      sed -i \"\" '/^alias ne=/d' "$HOME/.bashrc"
              fi

              echo "alias ne='QT_QUICK_BACKEND=software LIBGL_ALWAYS_SOFTWARE=1 ${emacsWithPackages}/bin/emacs --init-dir \"$EMACS_DIR\"'" >> "$HOME/.bashrc"

              # 提示用户手动 source 而不是直接执行,以避免 shell 继承问题
              echo "请手动运行 'source ~/.bashrc' 以使 alias 生效"
              echo "Emacs 配置已同步到 $EMACS_DIR"
              '';  
              });
}

5. 配置本体

5.1. Early-init

;; 增加 GC 阈值,加快启动
(setq gc-cons-threshold 402653184 gc-cons-percentage 0.6)

;; 启动完成后恢复正常 GC 设定
(add-hook 'emacs-startup-hook
          (lambda ()
            (setq gc-cons-threshold 10485760
                  gc-cons-percentage 0.1)))

;; 禁用bidi,加速大文件
(setq-default bidi-display-reordering nil)
(setq bidi-inhibit-bpa t
      long-line-threshold 1000
      large-hscroll-threshold 1000
      syntax-wholeline-max 1000)

5.2. Emacs基建

5.2.1. Mac OS 键位设定

;; 在mac中使用Command key作为meta
(setq mac-option-key-is-meta nil
      mac-command-key-is-meta t
      mac-command-modifier 'meta
      mac-option-modifier 'none)

;; 便于使用mac的JIS日语键盘
(global-set-key (kbd "C-¥") 'toggle-input-method)

5.2.2. 包管理和其他基础

这里我考虑了到底是否要使用 use-package 。因为我现在在使用 org mode 写配置文件的目的是希望可以穿插自然语言的代码描述,所以需要拆分代码。 use-package 这种一体成型的写法不是很适合这种风格。但考虑了实际写的过程,我觉得大多数情况可以在配置代码前面作完整的整理,而不需要过于细粒度的代码表述,然后在迁移的过程可能也比较简单。所以我最后还是决定改成使用 use-package 了。

  1. 依赖
    • use-package :用简介的宏语言描述包
    • diminish :用于隐藏一些 minor mode
    use-package
      diminish
    
  2. 配置
    (require 'use-package)
    (require 'diminish)
    

5.2.3. 更好的默认设置

  1. 本体的设定
    ;; 关闭警告声
    (setq ring-bell-function 'ignore)
    
    ;; 确认使用y或n,而不是yes或no。
    (defalias 'yes-or-no-p 'y-or-n-p)
    
    ;; 不自动生成备份文件
    (setq make-backup-files nil)
    
    ;; 选中文字能被整体替换(与其他文本编辑器相同)
    (delete-selection-mode 1)
    
    ;; 文件最后添加新行
    (setq require-final-newline t)
    
    ;; 文件在外部更新时buffer更新
    (global-auto-revert-mode 1)
    
  2. 优化长文档 (so-long)
    1. 依赖
      so-long
      
    2. 配置
      (use-package so-long
        :init
        (global-so-long-mode +1))
      
  3. UI
    1. 基础
      ;; 基础设置
      (tool-bar-mode -1) ;; 关闭工具栏
      (scroll-bar-mode -1) ;; 关闭文件滑动控件
      (setq inhibit-splash-screen 1) ;; 关闭启动帮助画面
      (setq initial-frame-alist (quote ((fullscreen . maximized)))) ;; 全屏
      (setq initial-scratch-message nil) ;; 关闭scratch message
      (setq inhibit-startup-message t) ;; 关闭启动信息
      (setq frame-title-format
            ;; 窗口显示文件路径/buffer名
            '("" " idiig - "
              (:eval (if (buffer-file-name)
                         (abbreviate-file-name (buffer-file-name)) "%b"))))
      (setq ns-use-proxy-icon nil)  ;; 删除frame icon
      
    2. 主题
      (require-theme 'modus-themes)
      
    3. 写作和展示UI(手动开启)
      1. 依赖
        spacious-padding
          writeroom-mode
        
      2. 配置
        (use-package spacious-padding
          :config
          (setq spacious-padding-widths
                '( :internal-border-width 15
                   :header-line-width 4
                   :mode-line-width 6
                   :tab-width 4
                   :right-divider-width 30
                   :scroll-bar-width 8))
        
          ;; Read the doc string of `spacious-padding-subtle-mode-line' as it
          ;; is very flexible and provides several examples.
          (setq spacious-padding-subtle-mode-line
                `( :mode-line-active 'default
                   :mode-line-inactive vertical-border)))
        
  4. 光标跳到新窗口

    emacs在打开新的窗口时,默认光标维持在原来的窗口。比如当你使用 describe-function 时,光标不会跳到函数的简介窗口。在这类窗口我们本身可以按 q 来退出和关闭窗口。所以跳转到新窗口非常便利。

    • 专用buffer(display-buffer行为;主要影响 Emacs 自动创建的窗口(如 help、compilation 等)。注意这里也会影响到 magit 这类 transient 窗口
    (setq switch-to-buffer-obey-display-actions t)
    (setq switch-to-buffer-in-dedicated-window 'pop)
    (customize-set-variable 'display-buffer-base-action
                            '((display-buffer-reuse-window display-buffer-same-window)
                              (reusable-frames . t)))
    
    • split-window时转跳到新窗口
    (defadvice split-window-below (after split-window-below-and-switch activate)
      "切换到新分割的窗口"
      (when (called-interactively-p 'any)
        (other-window 1)))
    
    (defadvice split-window-right (after split-window-right-and-switch activate)
      "切换到新分割的窗口"
      (when (called-interactively-p 'any)
        (other-window 1)))
    
  5. 窗口的放大缩小转变为持续的行为

    而不是要一直要重复 C-x 按键。后续行为使用默认 ^, V, {, } 。这里我没用 C-x v 是因为这个键位目前用于 vc

    (global-set-key (kbd "C-x V") 'shrink-window)
    
    (defun idiig/window-adjust-advice (orig-fun &rest args)
      "使用 Emacs 风格按键 (^, V, {, }, +) 持续调整窗口大小。"
      (let* ((ev last-command-event)
             (echo-keystrokes nil))
        ;; 执行初始调整
        (apply orig-fun args)
    
        ;; 设置 transient map
        (let ((delta (car args))) 
          (set-transient-map
           (let ((map (make-sparse-keymap)))
             ;; 垂直调整
             (define-key map (kbd "^")
                         `(lambda () (interactive) (enlarge-window ,delta nil)))
             (define-key map (kbd "V")
                         `(lambda () (interactive) (shrink-window ,delta nil)))
    
             ;; 水平调整
             (define-key map (kbd "{")
                         `(lambda () (interactive) (shrink-window ,delta t)))
             (define-key map (kbd "}")
                         `(lambda () (interactive) (enlarge-window ,delta t)))
    
             ;; 平衡窗口
             (define-key map (kbd "+")
                         (lambda () (interactive) (balance-windows)))
             ;; 最大化窗口
             (define-key map (kbd "M")
                         (lambda () (interactive) (maximize-window)))
             ;; 最小化窗口
             (define-key map (kbd "m")
                         (lambda () (interactive) (minimize-window)))
             map)
           nil nil
           "Use %k for further adjustment"))))
    
    ;; 添加 advice
    (advice-add 'enlarge-window :around #'idiig/window-adjust-advice)
    (advice-add 'shrink-window :around #'idiig/window-adjust-advice)
    (advice-add 'enlarge-window-horizontally :around #'idiig/window-adjust-advice)
    (advice-add 'shrink-window-horizontally :around #'idiig/window-adjust-advice)
    (advice-add 'maximize-window :around #'idiig/window-adjust-advice)
    (advice-add 'minimize-window :around #'idiig/window-adjust-advice)
    
  6. 文件的保存与新建
    ;; 不存在文档时询问是否新建
    (add-hook 'before-save-hook
              (lambda ()
                (when buffer-file-name
                  (let ((dir (file-name-directory buffer-file-name)))
                    (when (and (not (file-exists-p dir))
                               (y-or-n-p (format "Directory %s does not exist. Create it?" dir)))
                      (make-directory dir t))))))
    
    ;; 找文件时若无母文档则新建 
    (defadvice find-file (before make-directory-maybe
                                 (filename &optional wildcards) activate)
      "Create parent directory if not exists while visiting file."
      (unless (file-exists-p filename)
        (let ((dir (file-name-directory filename)))
          (when dir
            (unless (file-exists-p dir)
              (make-directory dir t))))))
    
  7. 最近文件
    (use-package recentf
      :defer t
      :commands
      (consult-recent-file)
      :init
      (setq recentf-save-file (expand-file-name "recentf" user-emacs-directory)
            recentf-max-saved-items 500
            recentf-max-menu-items 10)
      (setq recentf-exclude
            '("COMMIT_MSG"
              "COMMIT_EDITMSG"
              "github.*txt$"
              "/tmp/"
              "/sudo:"
              "/TAGS$"
              "/GTAGS$"
              "/GRAGS$"
              "/GPATH$"
              "\\.mkv$"
              "\\.mp[34]$"
              "\\.avi$"
              "\\.sub$"
              "\\.srt$"
              "\\.ass$"
              ".*png$"
              "Nutstore/org-files/"
              "bookmarks"))
      (setq recentf-max-saved-items 2048)
      (recentf-mode 1))
    
    ;; cleanup recent files
    (defun idiig/cleanup-recentf ()
      (progn
        (and (fboundp 'recentf-cleanup)
             (recentf-cleanup))))
    (add-hook 'kill-emacs-hook #'idiig/cleanup-recentf)
    
    • 自动保存文件设置
    (use-package savehist
      :init
      (setq savehist-additional-variables
            ;; search entries
            '(search-ring regexp-search-ring)
            ;; 每一分钟保存一次
            savehist-autosave-interval 60
            ;; keep the home clean
            savehist-file (expand-file-name "savehist" user-emacs-directory))
      (savehist-mode t))
    
    • 书签功能,打开时自动到原先编辑的位置
    (use-package bookmark
      :init
      (setq bookmark-default-file (expand-file-name "bookmarks" user-emacs-directory)
            bookmark-save-flag 1))
    
    • 保存文件的编辑位置
    (use-package saveplace
      :init
      (setq save-place-file (expand-file-name "place" user-emacs-directory))
      (save-place-mode 1))
    
  8. 文件管理 (Dired)
    (use-package dired
      :commands (dired dired-jump)
      :custom
      (dired-listing-switches "-alh")	; Use human-readable sizes
      (dired-dwim-target t)		    ; Try to be smart about guessing target directory
      (dired-recursive-deletes 'always) ; Always delete and copy recursively
      (dired-recursive-copies 'always)  ; Always delete and copy recursively
      :hook
      (dired-mode . auto-revert-mode)
      :config
      ;; Enable some commands by default
      (put 'dired-find-alternate-file 'disabled nil)
      (put 'dired-find-alternate-file 'disabled nil))
    
  9. 便利的光标首尾移动
    1. 依赖
      mwim
      
    2. 配置

      mwim: 跳到代码之前而非最前,或者代码后面而不是最后

      (use-package mwim
        :bind
        ("C-a" . mwim-beginning-of-code-or-line-or-comment)
        ("C-e" . mwim-end-of-code-or-line)
        :commands
        (mwim-beginning-of-code-or-line-or-comment
         mwim-end-of-code-or-line))
      
  10. 折行与复原
    1. 依赖
      unfill
      
    2. 配置
      • 物理折行与复原
      (use-package unfill
        :bind
        ("M-q" . unfill-toggle)
        :commands
        (unfill-toggle))
      
  11. 更好的minibuffer
    1. 原生设定
      (use-package emacs
        :init
        (progn
          ;; 为`completing-read-multiple'添加提示,比如[CRM<separator>]
          (defun crm-indicator (args)
            (cons (format "[CRM%s] %s"
                          (replace-regexp-in-string
                           "\\`\\[.*?]\\*\\|\\[.*?]\\*\\'" ""
                           crm-separator)
                          (car args))
                  (cdr args)))
          (advice-add #'completing-read-multiple :filter-args #'crm-indicator)
      
          ;; 不允许鼠标出现在minibuffer的提示中
          (setq minibuffer-prompt-properties
                '(read-only t cursor-intangible t face minibuffer-prompt))
          (add-hook 'minibuffer-setup-hook #'cursor-intangible-mode)
      
          ;; 在emacs 28以后,非当前mode的指令都会被隐藏,vertico的指令也会隐藏
          (setq read-extended-command-predicate
                #'command-completion-default-include-p)
      
          ;; minibuffer可循环
          (setq enable-recursive-minibuffers t)))
      
      ;; http://trey-jackson.blogspot.com/2010/04/emacs-tip-36-abort-minibuffer-when.html
      ;; 使用鼠标时关闭minibuffer
      (defun idiig/stop-using-minibuffer ()
        "kill the minibuffer"
        (when (and (>= (recursion-depth) 1) (active-minibuffer-window))
          (abort-recursive-edit)))
      (add-hook 'mouse-leave-buffer-hook 'idiig/stop-using-minibuffer)
      
      
    2. 基础五件套
      1. 依赖
        vertico
          orderless
          marginalia
          embark
          consult
          embark-consult
        
      2. 配置
        1. Vertico: 提供minibuffer补全UI
          (use-package vertico
            :after consult
            :custom
            (vertico-count 9)
            (vertico-cycle t)
            :init
            (vertico-mode))
          
        2. Orderless: 提供补全格式选择
          (use-package orderless
            :after
            (consult)
            :init
            (defvar +orderless-dispatch-alist
            '((?% . char-fold-to-regexp)    ; %word% - 字符折叠匹配
              (?! . orderless-without-literal) ; !word! - 排除匹配
              (?`. orderless-initialism)    ; `word` - 首字母匹配
              (?= . orderless-literal)      ; =word= - 字面匹配
              (?~ . orderless-flex)))	  ; ~word~ - 弹性匹配
            :config
            (setq search-default-mode t)
          
            (defun +orderless--suffix-regexp ()
              (if (and (boundp 'consult--tofu-char) (boundp 'consult--tofu-range))
                  (format "[%c-%c]*$"
                          consult--tofu-char
                          (+ consult--tofu-char consult--tofu-range -1))
                "$"))
            ;; Recognizes the following patterns:
            ;; * ~flex flex~
            ;; * =literal literal=
            ;; * %char-fold char-fold%
            ;; * `initialism initialism`
            ;; * !without-literal without-literal!
            ;; * .ext (file extension)
            ;; * regexp$ (regexp matching at end)
            (defun +orderless-dispatch (word _index _total)
              (cond
               ;; Ensure that $ works with Consult commands, which add disambiguation suffixes
               ((string-suffix-p "$" word)
                `(orderless-regexp . ,(concat (substring word 0 -1) (+orderless--suffix-regexp))))
               ;; File extensions
               ((and (or minibuffer-completing-file-name
                         (derived-mode-p 'eshell-mode))
                     (string-match-p "\\`\\.." word))
                `(orderless-regexp . ,(concat "\\." (substring word 1) (+orderless--suffix-regexp))))
               ;; Ignore single !
               ((equal "!" word) `(orderless-literal . ""))
               ;; Prefix and suffix
               ((if-let (x (assq (aref word 0) +orderless-dispatch-alist))
                    (cons (cdr x) (substring word 1))
                  (when-let (x (assq (aref word (1- (length word))) +orderless-dispatch-alist))
                    (cons (cdr x) (substring word 0 -1)))))))
          
            ;; Define orderless style with initialism by default ; add migemo feature for japanese
            (orderless-define-completion-style +orderless-with-initialism
              (orderless-matching-styles '(orderless-initialism
                                           orderless-literal
                                           orderless-regexp)))
          
            (setq completion-styles '(orderless basic)
                  completion-category-defaults nil
                  ;;; Enable partial-completion for files.
                  ;;; Either give orderless precedence or partial-completion.
                  ;;; Note that completion-category-overrides is not really an override,
                  ;;; but rather prepended to the default completion-styles.
                  ;; completion-category-overrides '((file (styles orderless partial-completion))) ;; orderless is tried first
                  completion-category-overrides '((file (styles partial-completion)) ;; partial-completion is tried first
                                                  (buffer (styles +orderless-with-initialism))
                                                  (consult-location (styles +orderless-with-initialism))
                                                  ;; enable initialism by default for symbols
                                                  (command (styles +orderless-with-initialism))
                                                  (variable (styles +orderless-with-initialism))
                                                  (symbol (styles +orderless-with-initialism)))
                  orderless-component-separator #'orderless-escapable-split-on-space ;; allow escaping space with backslash!
                  orderless-style-dispatchers '(+orderless-dispatch)))
          
        3. Maginalia: 增强minubuffer的annotation
          (use-package marginalia
            :after vertico
            ;; 只在minibuffer启用快捷键
            :bind (:map minibuffer-local-map ("M-A" . marginalia-cycle))
            :init
            (setq marginalia-align-offset 5)
            :config
            (marginalia-mode))
          
        4. Consult: 增强minibuffer的检索
          (use-package consult
            :hook (after-init . (lambda () (require 'consult)))
            :bind (([remap M-x] . execute-extended-command)
                   ([remap goto-line] . consult-goto-line)
                   ([remap switch-to-buffer] . consult-buffer)
                   ([remap find-file] . find-file)
                   ([remap imenu] . consult-imenu)
                   ("C-c r" . consult-recent-file)
                   ("C-c y" . consult-yasnippet)
                   ("C-c f" . consult-find)
                   ("C-c s" . consult-line)
                   ("C-c o" . consult-file-externally)
                   ("C-c p f" . consult-ripgrep)
                   (:map minibuffer-local-map
                         ("C-c h" . consult-history)
                         ("C-s" . #'previous-history-element)))
            :init
            (add-to-list 'exec-path "${pkgs.fd}/bin")
            (add-to-list 'exec-path "${pkgs.ripgrep}/bin")
            (defun idiig/consult-buffer-region-or-symbol ()
              "consult-line当前字符或选中区域."
              (interactive)
              (let ((input (if (region-active-p)
                               (buffer-substring-no-properties
                                (region-beginning) (region-end))
                             (thing-at-point 'symbol t))))
                (consult-line input)))
            (defun idiig/consult-project-region-or-symbol (&optional default-inputp)
              "consult-ripgrep 当前字符或选中区域."
              (interactive)
              (let ((input (if (region-active-p)
                               (buffer-substring-no-properties
                                (region-beginning) (region-end))
                             (thing-at-point 'symbol t))))
                (consult-ripgrep default-inputp input)))
            :config
            (progn
              ;; (defvar my-consult-line-map
              ;;   (let ((map (make-sparse-keymap)))
              ;;     (define-key map "C-s" #'previous-history-element)
              ;;     map))
              ;; (consult-customize consult-line :keymap my-consult-line-map)
              ;; ;; 禁止自动显示consult文件的内容
              (setq consult-preview-key "C-v")
              ;; 应用 Orderless 的正则解析到 consult-grep/ripgrep/find
              (defun consult--orderless-regexp-compiler (input type &rest _config)
                (setq input (orderless-pattern-compiler input))
                (cons
                 (mapcar (lambda (r) (consult--convert-regexp r type)) input)
                 (lambda (str) (orderless--highlight input str))))
              ;; 表示的buffer种类
              (defcustom consult-buffer-sources
                '(consult--source-hidden-buffer
                  consult--source-buffer
                  consult--source-file
                  consult--source-bookmark
                  consult--source-project-buffer
                  consult--source-project-file)
                "Sources used by `consult-buffer'. See `consult--multi' for a description of the source values."
                :type '(repeat symbol))
              ;; ?提示检索buffer类型;f<SPC>=file, p<SPC>=project, etc..
              (define-key consult-narrow-map
                          (vconcat consult-narrow-key "?") #'consult-narrow-help)))
          
        5. Embark: minibuffer action 和自适应的context menu
          (use-package embark
            :after vertico
            :bind
            (("C-h B" . embark-bindings)  ;; alternative for `describe-bindings'
             (:map minibuffer-local-map
                   ("C-'" . embark-act)         ;; 对函数进行设置操作 
                   ("M-." . embark-dwim)        ;; 实施 
                   ("C-c C-e" . embark-export))) ;; occur 
            :init
            ;; Optionally replace the key help with a completing-read interface
            (setq prefix-help-command #'embark-prefix-help-command)
            :config
            (add-to-list 'display-buffer-alist
                         '("\\`\\*Embark Collect \\(Live\\|Completions\\)\\*"
                           nil
                           (window-parameters (mode-line-format . none)))))
          
          ;; embark-export弹出occur和grep mode的buffer
          (use-package embark-consult
            :ensure t
            :after (consult))
          
  12. 撤销 (vundo)

    我原来使用 undotree ,现在使用 vundo。这些用于视觉化撤销树。这里我之绑定了 C-x uC-/ 我依然用的原生的 Undo,这样适合区分使用。

    1. 依赖
      vundo
      
    2. 配置
      (use-package vundo
        :defer t
        :commands
        (vundo)
        :bind
        ("C-x u" . vundo))
      
  13. 检索 (ctrlf)

    针对当前 buffer 利用 Ctrlf 而不在使用 swiperhelm 这类型的检索方式。用于替代 isearchC-s 键。另外的选项是 consult-line ,我映射到了 C-c s 键位,用于不移动当前位置预览检索行,尤其是在类似与 org 存在折叠的情况下,我不需要移动光标和展开折叠。关于 consult ,可见Consult: 增强 minibuffer的检索

    1. 依赖
      ctrlf
      
    2. 配置
      1. 启动 ctrf
        (require 'ctrlf)
        (ctrlf-mode +1)
        
      2. 切换检索风格
        (with-eval-after-load 'ctrlf
        
          ;; 定义 advice 函数
          (defun ctrlf-set-default-style-advice (style)
            "Advice function to set the default search style when changing styles.
        This ensures the selected style becomes the new default for future sessions."
            (setq ctrlf-default-search-style style))
        
          ;; 添加 advice
          (advice-add 'ctrlf-change-search-style :after #'ctrlf-set-default-style-advice))
        
  14. 重构 (wgrep)

    grep , ag, ripgrep 等检索的结果中按下 e 进入编辑模式,按下 C-c C-c 完成修改。

    1. 依赖
      wgrep
      
    2. 配置
      (use-package wgrep
        :config
        (setq wgrep-auto-save-buffer t)
        (setq wgrep-enable-key "e"))
      
  15. 括号匹配
    (use-package emacs
      :init
      ;; 启用自动括号配对
      (electric-pair-mode t)
    
      :config
      ;; 配置 electric-pair-mode 行为
      (setq electric-pair-preserve-balance nil)
      ;; 使用保守的抑制策略
      ;; https://www.reddit.com/r/emacs/comments/4xhxfw/how_to_tune_the_behavior_of_eletricpairmode/
      (setq electric-pair-inhibit-predicate 'electric-pair-conservative-inhibit)
    
      ;; 保存默认的配对括号设置,以便创建模式特定的本地设置
      (defconst idiig/default-electric-pairs electric-pair-pairs)
    
      ;; 为特定模式添加本地电子配对
      (defun idiig/add-local-electric-pairs (pairs)
        "为当前缓冲区添加本地电子配对括号。
    
         参数:
           PAIRS: 要添加的括号对列表
    
         示例用法:
           (add-hook 'jupyter-org-interaction-mode-hook
                     (lambda () (idiig/add-local-electric-pairs '((?$ . ?$)))))"
        (setq-local electric-pair-pairs (append idiig/default-electric-pairs pairs))
        (setq-local electric-pair-text-pairs electric-pair-pairs))
    
      ;; 禁止自动配对尖括号 <>
      (add-function :before-until electric-pair-inhibit-predicate
                    (lambda (c) (eq c ?<)))
    
      ;; 增强的括号匹配高亮——即使光标在括号内也能高亮匹配的括号
      (define-advice show-paren-function (:around (fn) fix-show-paren-function)
        "即使光标不直接位于括号上,也能高亮匹配的括号。"
        (cond ((looking-at-p "\\s(") (funcall fn))
              (t (save-excursion
                   (ignore-errors (backward-up-list))
                   (funcall fn)))))
    
      ;; 启用括号匹配高亮
      (show-paren-mode t))
    
  16. 语言无关的结构化编程 (puni)
    1. 依赖
      puni
      
    2. 配置
      (use-package puni
        :defer t
        :bind
        (:map puni-mode-map
              ([remap puni-kill-line] . idiig/puni-kill-line)
              ("C--" . idiig/puni-contract-region)
              ("C-=" . puni-expand-region))
        :init
        ;; The autoloads of Puni are set up so you can enable `puni-mode` or
        ;; `puni-global-mode` before `puni` is actually loaded. Only after you press
        ;; any key that calls Puni commands, it's loaded.
        (puni-global-mode)
        (add-hook 'term-mode-hook #'puni-disable-puni-mode)
        :config
        (defun idiig/puni-kill-line (&optional n)
          "Kill a line forward while keeping expressions balanced.
        If forward kill is not possible, try backward. If still nothing
        can be deleted, kill the balanced expression around point."
          (interactive "p")
          (let ((bounds (puni-bounds-of-list-around-point)))
            (cond
             ;; Case 1: No list bounds found, try deleting surrounding sexp
             ((null bounds)
              (when-let ((sexp-bounds (puni-bounds-of-sexp-around-point)))
                (puni-delete-region (car sexp-bounds) (cdr sexp-bounds) 'kill)))
      
             ;; Case 2: Point is at end of bounds, try backward kill
             ((eq (point) (cdr bounds))
              (puni-backward-kill-line))
      
             ;; Case 3: Default forward kill
             (t
              (puni-kill-line n)))))
        (defun idiig/puni-contract-region (&optional arg)
          "如无选中则保持 negative-argument,如有选中则缩小范围"
          (interactive "p")
          (if (region-active-p)
              (call-interactively #'puni-contract-region)
            (negative-argument arg)))
      
      )
      
      1. puni-kill-line

        基于 puni 更改 kill-line ,在删除行的时候可以确认是否被包围在某个环境中。如果被包围在某个环境中则删除到该环境的最后,如果没有则正常执行 kill-line

        (defun idiig/puni-kill-line (&optional n)
          "Kill a line forward while keeping expressions balanced.
        If forward kill is not possible, try backward. If still nothing
        can be deleted, kill the balanced expression around point."
          (interactive "p")
          (let ((bounds (puni-bounds-of-list-around-point)))
            (cond
             ;; Case 1: No list bounds found, try deleting surrounding sexp
             ((null bounds)
              (when-let ((sexp-bounds (puni-bounds-of-sexp-around-point)))
                (puni-delete-region (car sexp-bounds) (cdr sexp-bounds) 'kill)))
        
             ;; Case 2: Point is at end of bounds, try backward kill
             ((eq (point) (cdr bounds))
              (puni-backward-kill-line))
        
             ;; Case 3: Default forward kill
             (t
              (puni-kill-line n)))))
        
      2. idiig/puni-contract-region

        如无选中则保持 negative-argument,如有选中则缩小范围

        (defun idiig/puni-contract-region (&optional arg)
          "如无选中则保持 negative-argument,如有选中则缩小范围"
          (interactive "p")
          (if (region-active-p)
              (call-interactively #'puni-contract-region)
            (negative-argument arg)))
        
        ;; 添加 advice
        (with-eval-after-load 'puni
          (defun idiig/puni-expand-region-advice (orig-fun &rest args)
            "使用选中后的操作"
        
            (let* ((ev last-command-event)
                   (echo-keystrokes nil))
              ;; 执行初始调整
              (apply orig-fun args)
        
              ;; 设置 transient map
              (let ((delta (car args))) 
                (set-transient-map
                 (let ((map (make-sparse-keymap)))
                   ;; 持续扩大
                   (define-key map (kbd "=") 'puni-expand-region)
                   ;; 缩小范围
                   (define-key map (kbd "-") 'puni-contract-region)
                   ;; 其他操作
                   ;; 检索
                   (define-key map (kbd "/") 'idiig/consult-project-region-or-symbol)
                   (define-key map (kbd "b") 'idiig/consult-buffer-region-or-symbol)
                   ;; 加包围
                   (define-key map (kbd ")") 'puni-wrap-round)
                   (define-key map (kbd "]") 'puni-wrap-square)
                   (define-key map (kbd "}") 'puni-wrap-curly)
                   (define-key map (kbd ">") 'puni-wrap-angle)
                   map)
                 nil nil
                 "Use %k for further adjustment"))))
          (advice-add 'puni-expand-region :around #'idiig/puni-expand-region-advice))
        
  17. 覆盖 <Backspace><DEL> 等删除动作
    1. backward-hungry-delete

      向后删除时向后贪婪地删除连续的空白值。同时考虑对称的结构。

      • 首先检查光标前面是否有连续的空白字符。
      • 使用 looking-back 用于判断满足以下任何一个条件:
        • 光标之前在当前行是否有符合正则表达式 (+ blank) 的字符序列。
        • 光标是否在行首 (bolp)
      • 如果有连续的空白字符或在行首:
        • 使用 skip-chars-backward 向后跳过这些字符,并记录开始位置 start
        • 然后 delete-region 用于删除从 start 到当前光标位置之间的字符。
      (defun idiig/backward-hungry-delete-advice (orig-fun &rest args)
        "Advice function to provide hungry delete functionality."
        (if (or (looking-back (rx (+ blank))) (bolp))
            (let ((start (save-excursion (skip-chars-backward " \t\f\n\r\v") (point))))
              (delete-region start (point)))
          (apply orig-fun args)))
      
      (defun idiig/apply-backward-hungry-delete-advice ()
        "Reapply the hungry delete advice to the current DEL key binding function."
        (let ((current-fun (key-binding (kbd "DEL"))))
          (advice-remove current-fun #'idiig/backward-hungry-delete-advice)    ; 移除旧的 advice
          (advice-add current-fun :around #'idiig/backward-hungry-delete-advice))) ; 应用新的 advice
      
      ;; 在 emacs-startup 时应用 advice
      (add-hook 'emacs-startup-hook #'idiig/apply-backward-hungry-delete-advice)
      
      ;; 如果你有其他 hook 如打开某种模式时,需要重新应用 advice,可添加对应 hook,例如:
      ;; (add-hook 'your-major-mode-hook #'idiig/reapply-backward-hungry-delete-advice)
      
    2. forward-hungry-delete

      向前删除时向前贪婪地删除连续的空白值。同时考虑对称的结构。

      • 检查光标后的字符:
        • 使用 looking-at 判断光标后面的字符是否是一个或多个空白字符或换行符。
        • 如果匹配到,使用 skip-chars-forward 跳过所有这些字符并记录结束位置。
        • 使用 delete-region 删除从当前光标位置到记录的结束位置之间的所有空白。
      • 字符删除逻辑:
        • 如果光标后没有多余的空白字符,使用 dotimes 循环和 puni-forward-delete-char 删除 n 个普通字符。
        • unless (eobp): 确保在没有到达缓冲区末尾时进行字符删除,防止出现试图超出缓冲区范围的错误。
      (defun idiig/forward-hungry-delete-advice (orig-fun &rest args)
        "Advice function to provide forward hungry delete functionality."
        (if (looking-at (rx (or (1+ blank) "\n")))
            (let ((end (save-excursion
                         (skip-chars-forward " \t\f\v\n\r")
                         (point))))
              (delete-region (point) end))
          (apply orig-fun args)))
      
      (defun idiig/apply-forward-hungry-delete-advice ()
        "Apply the forward hungry delete advice to the current forward delete key binding function."
        (let ((current-fun (key-binding (kbd "C-d"))))
          (advice-remove current-fun #'idiig/forward-hungry-delete-advice) ; 移除旧的 advice
          (advice-add current-fun :around #'idiig/forward-hungry-delete-advice))) ; 应用新的 advice
      
      ;; 在 emacs-startup 时应用 advice
      (add-hook 'emacs-startup-hook #'idiig/apply-forward-hungry-delete-advice)
      
      ;; 如果你有其他 hook 需要重新应用 advice,可添加对应 hook,例如:
      ;; (add-hook 'your-major-mode-hook #'idiig/apply-forward-hungry-delete-advice)
      
    3. backward-kill-word-or-region

      如无选中则杀掉前面的单词,如有选中则杀掉选中区域。

      (defun idiig/backward-kill-word-or-region-advice (orig-fun &rest args)
        "Enhance the C-w function to handle region more flexibly."
        (if (region-active-p)
            ;; 当有选中区域时,使用传递的参数调用原始C-w功能(例如 `puni-kill-region`)
            (apply orig-fun args)
          ;; 当没有选中区域时,执行删除单词操作
          (let ((backward-kill-word-fun (or (key-binding (kbd "M-<DEL>"))
                                            (key-binding (kbd "S-<delete>"))
                                            'backward-kill-word))) ; 默认删除单词函数
            (if (fboundp backward-kill-word-fun)
                (call-interactively backward-kill-word-fun) ; 交互式调用删除单词
              (message "No word kill bound function found for M-<DEL> or S-<delete>")))))
      
      (defun idiig/apply-backward-kill-word-or-region-advice ()
        "Advice C-w to optionally kill region or word."
        ;; 通过 `key-binding` 得到当前与 C-w 绑定的函数
        (let ((current-fun (key-binding (kbd "C-w"))))
          (advice-remove current-fun #'idiig/backward-kill-word-or-region-advice)
          (advice-add current-fun :around #'idiig/backward-kill-word-or-region-advice)))
      
      ;; 在 emacs 启动时应用这个 advice
      (add-hook 'emacs-startup-hook #'idiig/apply-backward-kill-word-or-region-advice)
      

5.3. CJK字体

这里我统一使用的是Sarasa的等宽字体,可以避免2个问题:

  • 输入latin以后输入cjk文字以后,由于字体高度不等导致行高抖动
  • 方便org等表格等宽表示

5.3.1. 依赖

if [ "$(uname)" = "Darwin" ]; then
    # macOS
    mkdir -p "$HOME/Library/Fonts/"
    ${pkgs.rsync}/bin/rsync -av ${pkgs.sarasa-gothic}/share/fonts/truetype/ "$HOME/Library/Fonts/"
else
    # Assume Linux
    mkdir -p "$HOME/.local/share/fonts/truetype/"
    ${pkgs.rsync}/bin/rsync -av ${pkgs.sarasa-gothic}/share/fonts/truetype/ "$HOME/.local/share/fonts/sarasa-gothic/"
    fc-cache -f -v ~/.local/share/fonts/
fi

5.3.2. 配置

(add-hook 'after-init-hook
          (lambda ()
            (let* ((screen-height (display-pixel-height))
                   (font-height (if (> screen-height 1200) 230 130))  ;; 根据屏幕高度调整
                   (minibuffer-font-height (- font-height 0))
                   (my-font "Sarasa Mono SC"))
              (set-face-attribute 'default nil :family my-font :height font-height)
              ;; 设置 mode-line 字体
              (set-face-attribute 'mode-line nil :family my-font :height font-height)
              (set-face-attribute 'mode-line-inactive nil :family my-font :height font-height)
              ;; 设置 minibuffer 字体
              (set-face-attribute 'minibuffer-prompt nil :family my-font :height minibuffer-font-height))))

;; 工具栏,菜单保持默认字体
(set-face-attribute 'menu nil :inherit 'unspecified)
(set-face-attribute 'tool-bar nil :inherit 'unspecified)

5.4. 日文

5.4.1. 输入法 (ddskk)

  1. 依赖
    ddskk
    
  2. 配置
    (use-package ddskk
      :defer t
      :bind (("C-x j" . skk-mode))
      :config
      (setq skk-server-inhibit-startup-server nil)
      (setq skk-server-host "localhost")
      (setq skk-server-portnum 55100)
      (setq skk-share-private-jisyo t)
    
      ;; 候补显示设置
      (setq skk-show-inline t)
      (setq skk-show-tooltip t)
      (setq skk-show-candidates-always-pop-to-buffer t)
      (setq skk-henkan-show-candidates-rows 2)
    
      ;; 行为设置
      (setq skk-egg-like-newline t)
      (setq skk-delete-implies-kakutei nil)
      (setq skk-use-look t)
      (setq skk-auto-insert-paren t)
      (setq skk-henkan-strict-okuri-precedence t)
    
      ;; 片假名转换设置
      (setq skk-search-katakana 'jisx0201-kana)
    
      ;; 加载额外功能
      (require 'skk-hint)
      :hook
      (skk-load . (lambda ()
                    (require 'context-skk))))
    

5.4.2. 检索(Migemo)

  1. 依赖
    migemo
    

    注意这里使用的是 cmigemo

  2. 配置
    1. 基础配置
      (require 'migemo)
      ;; cmigemo(default)
      (setq migemo-command "${pkgs.cmigemo}/bin/cmigemo")
      (setq migemo-options '("-q" "--emacs"))
      
      ;; Set your installed path
      (setq migemo-dictionary "${pkgs.cmigemo}/share/migemo/utf-8/migemo-dict")
      
      (setq migemo-user-dictionary nil)
      (setq migemo-regex-dictionary nil)
      (when (and migemo-command migemo-dictionary)
        (migemo-init)
        (message "Migemo initialized with dictionary: %s" migemo-dictionary))
      
    2. buffer内字符检索 (Ctrlf) 交互
      (with-eval-after-load 'migemo
        (with-eval-after-load 'ctrlf
          (add-to-list 'ctrlf-style-alist '(migemo-regexp . (:prompt "migemo-regexp"
                                                                     :translator migemo-search-pattern-get
                                                                     :case-fold ctrlf-no-uppercase-regexp-p)))))
      
      
    3. minibuffer内检索 (Orderless) 交互

      migemo 在minibuffer的检索中用 # 前缀可开启罗马字检索日语。

      (with-eval-after-load 'orderless
        (defun orderless-migemo (component)
        (let ((pattern (migemo-get-pattern component)))
          (condition-case nil
              (progn (string-match-p pattern "") pattern)
            (invalid-regexp nil))))
      
        (add-to-list '+orderless-dispatch-alist '(?# . orderless-migemo)))
      

5.5. 中文

5.5.1. 输入法和基于输入法的检索 (pyim)

  1. 依赖
    pyim
      pyim-basedict
    
  2. 配置
    1. 基础配置
      (use-package pyim
        :diminish pyim-isearch-mode
        :commands
        (toggle-input-method)
        :custom
        (default-input-method "pyim")
        (pyim-dcache-directory (concat user-emacs-directory "pyim/dcache"))
        (pyim-default-scheme 'quanpin)
        (pyim-page-tooltip 'popup)
        (pyim-page-length 4))
      
      ;; 加载并启用基础词库
      (use-package pyim-basedict
        :after pyim
        :config
        (pyim-basedict-enable))
      
    2. TODO 正则表达交互

      目前支持:

      • 在minibuffer中用 C-Ret 把单字拼音转换为该读音本身代表的中文正则表达
      • M-x idiig/toggle-pyim-region 用于开关中文的forward-word和backward
      • 激活进入pyim时,自动开启中文的forward和backward
      (with-eval-after-load 'pyim
        (require 'pyim-cstring-utils)
      
        ;; C-return 把当前选中的位置转换为正则表达
        (define-key minibuffer-local-map (kbd "C-<return>") 'pyim-cregexp-convert-at-point)
      
        (defvar idiig/pyim-region-enabled nil
          "记录pyim区域功能是否启用的状态变量。")
      
        (defun idiig/toggle-pyim-region ()
          "切换pyim的单词移动功能。
      当启用时,会将forward-word和backward-word重映射为pyim的相应函数;
      当禁用时,会恢复原来的映射。"
          (interactive)
          (if idiig/pyim-region-enabled
              (progn
                (idiig/disable-pyim-region)
                (setq idiig/pyim-region-enabled nil)
                (message "已禁用pyim区域功能"))
            (progn
              (idiig/enable-pyim-region)
              (setq idiig/pyim-region-enabled t)
              (message "已启用pyim区域功能"))))
      
        (defun idiig/enable-pyim-region (&rest _)
          "启用pyim的单词移动建议。"
          (global-set-key [remap forward-word] 'pyim-forward-word)
          (global-set-key [remap backward-word] 'pyim-backward-word))
      
        (defun idiig/disable-pyim-region (&rest _)
          "禁用pyim的单词移动建议。"
          (global-unset-key [remap forward-word])
          (global-unset-key [remap backward-word]))
      
        ;; ;; 挂钩到 pyim 的启用/禁用钩子上
        ;; (advice-remove 'pyim-deactivate #'idiig/disable-pyim-region)
        ;; (advice-remove 'pyim-activate #'idiig/enable-pyim-region)
        ;; (advice-add 'pyim-deactivate :after #'idiig/disable-pyim-region)
        (advice-add 'pyim-activate :after #'idiig/enable-pyim-region))
      
    3. buffer内检索 (Ctrlf) 交互

      这里我写了一个函数 pyim-cregex-build-lazy 。这个函数交互 pyim 。参见输入法。这个函数的工作逻辑如下:

      • 如果还没有初始化拼音数据,就先进行预热预热时会加载 "a"、"e"、"o" 这三个字符的拼音映射数据设置初始化标志,避免重复初始化
      • 接着分两种情况处理:
        1. 情况1 :: 单个字符且不是 a/e/o,双个字母不是 zh/ch/sh
          1. 使用 regexp-quote 直接转义字符
          2. 例如:输入 "b" → 直接匹配字符 "b"
          3. 避免触发拼音转换,提高性能
        2. 情况2 :: 其他所有情况
          1. 使用 pyim-cregexp-build 进行拼音转换,包括:
            • 单个字符 "a"、"e"、"o"(常用韵母)
            • 多个字符组合(如 "zh"、"zhong")
      • 设计目的
        1. 性能优化 :: 避免输入大多数单个字符时的拼音转换开销
        2. 保持功能 :: 在需要拼音搜索时正常工作
        3. 用户体验 :: 减少首次输入时的卡顿感
      (with-eval-after-load 'ctrlf
      
        (defvar pyim-ctrlf-initialized nil
          "Flag to track if pyim data has been initialized for ctrlf.")
      
        (defvar pyim-ctrlf-cache (make-hash-table :test 'equal)
          "Cache for pyim-cregexp-build results.")
      
        (defconst pyim-ctrlf-vowels-with-mapping '("a" "e" "o")
          "Vowels that have direct Chinese character mappings.")
      
        (defconst pyim-ctrlf-double-consonants '("zh" "ch" "sh")
          "Double-letter consonants that should use regex-quote for exact matching.")
      
        (defun pyim-cregexp-build-lazy (str)
          "Lazy wrapper for pyim-cregexp-build with caching."
          (unless pyim-ctrlf-initialized
            (message "Initializing pyim data for ctrlf...")
            ;; 预缓存常用字符的结果
            (call-interactively #'pyim-activate)
            (call-interactively #'pyim-deactivate)
      
            (dolist (vowel pyim-ctrlf-vowels-with-mapping)
              (let ((result (pyim-cregexp-build vowel)))
                (puthash vowel result pyim-ctrlf-cache)))
            (setq pyim-ctrlf-initialized t)
            (message "Pyim data initialized."))
      
          ;; 判断是否使用 regex-quote
          (if (or (and (= (length str) 1)
                       (not (member str pyim-ctrlf-vowels-with-mapping)))
                  (member str pyim-ctrlf-double-consonants))
              (regexp-quote str)
            ;; 使用缓存或计算新结果
            (or (gethash str pyim-ctrlf-cache)
                (let ((result (pyim-cregexp-build str)))
                  (puthash str result pyim-ctrlf-cache)
                  result))))
      
        (add-to-list 'ctrlf-style-alist
                     '(pinyin-regexp . (:prompt "pinyin-regexp"
                                                :translator pyim-cregexp-build-lazy
                                                :case-fold ctrlf-no-uppercase-regexp-p
                                                :fallback (isearch-forward-regexp
                                                           . isearch-backward-regexp)))))
      
    4. minibuffer内检索 (Orderless) 交互

      pyim 在minibuffer的检索中用 前缀可开启拼音检索中文。

      ;; (with-eval-after-load 'orderless
      ;;   ;; 拼音检索字符串功能
      ;;   (defun zh-orderless-regexp (orig_func component)
      ;;     (call-interactively #'pyim-activate)
      ;;     (call-interactively #'pyim-deactivate)
      ;;     (let ((result (funcall orig_func component)))
      ;;       (pyim-cregexp-build result)))
      ;;   (advice-add 'orderless-regexp :around #'zh-orderless-regexp))
      
      (with-eval-after-load 'orderless
      
        (defvar pyim-orderless-initialized nil
          "Flag to track if pyim data has been initialized for orderless.")
      
        (defun orderless-pyim (component)
          (unless pyim-orderless-initialized
            (message "Initializing pyim for orderless...")
            ;; 预缓存常用字符的结果
            (call-interactively #'pyim-activate)
            (call-interactively #'pyim-deactivate)
            (setq pyim-orderless-initialized t)
            (message "Pyim data initialized."))
      
          (let ((pattern (pyim-cregexp-build component)))
            (condition-case nil
                (progn (string-match-p pattern "") pattern)
              (invalid-regexp nil))))
      
        (add-to-list '+orderless-dispatch-alist '(?@ . orderless-pyim)))
      

5.6. Git (magit)

用 Magit 进行项目与版本的管理

5.6.1. 依赖

magit

5.6.2. 配置

  1. Magit
    (use-package magit
      :bind ("C-x g" . magit-status)
      :commands magit-status
      :init
      ;; 使用nix路径中的git
      (add-to-list 'exec-path "${pkgs.git}/bin"))
    

5.7. 文档写作

5.7.1. 文档后缀

(defvar idiig/writing-environment-list '("\\.org\\'"
                                         "\\.md\\'"
                                         "\\.qmd\\'"
                                         "\\.rmd\\'"
                                         "\\.typ\\'"
                                         "\\.tex\\'"
                                         "\\.bib\\'"
                                         "\\.txt\\'"))

5.7.2. 文档状态折行

(defun idiig/in-writing-environment-p ()
  "Check if current buffer file matches any pattern in idiig/writing-environment-list."
  (when (buffer-file-name)
    (cl-some (lambda (pattern)
               (string-match-p pattern (buffer-file-name)))
             idiig/writing-environment-list)))

(add-hook 'find-file-hook
          (lambda ()
            (when (idiig/in-writing-environment-p)
              (visual-line-mode 1))))

(with-eval-after-load 'diminish
  (diminish 'visual-line-mode))

5.7.3. 在选中区域的状态下 C-w 删除选中的区域

在没选中的状态下删除上一个单词。

(with-eval-after-load 'puni
  (defun idiig/backward-kill-word-or-region (&optional arg)
    (interactive "p")
    (if (region-active-p)
        (call-interactively #'puni-kill-active-region)
      (backward-kill-word arg)))

  (global-set-key (kbd "C-w") 'idiig/backward-kill-word-or-region))

5.7.4. C-M-\ 全局缩进

(defun idiig/indent-buffer()
  (interactive)
  (indent-region (point-min) (point-max)))

(defun idiig/indent-region-or-buffer()
  (interactive)
  (save-excursion
    (if (region-active-p)
        (progn
          (indent-region (region-beginning) (region-end)))
      (progn
        (idiig/indent-buffer)))))

(global-set-key (kbd "C-M-\\") 'idiig/indent-region-or-buffer)
(global-set-key (kbd "C-M-¥") 'idiig/indent-region-or-buffer)  ;; JIS keyboard

5.7.5. Shift-Ret 下方插入空白行

(global-set-key [(shift return)] 'idiig/smart-open-line)

5.7.6. M-- 匹配到括号

TODO: 把 evil-jump-item 换成别的函数。

(defun idiig/goto-match-paren (arg)
  "Go to the matching if on (){}[], similar to vi style of % "
  (interactive "p")
  ;; first, check for "outside of bracket" positions expected by forward-sexp, etc
  (cond ((looking-at "[\[\(\{]") (evil-jump-item))
        ((looking-back "[\]\)\}]" 1) (evil-jump-item))
        ;; now, try to succeed from inside of a bracket
        ((looking-at "[\]\)\}]") (forward-char) (evil-jump-item))
        ((looking-back "[\[\(\{]" 1) (backward-char) (evil-jump-item))
        (t nil)))

(bind-key* "M--" 'idiig/goto-match-paren)

5.7.7. 点后插入空白

(defun idiig/insert-space-after-point ()
  (interactive)
  (save-excursion (insert " ")))

(bind-key* "C-." 'idiig/insert-space-after-point)

5.8. 编程工具

5.8.1. 我有可能使用的语言

;; TODO: 这里未来需要改成在每个语言的设定的节点push进来
(defvar idiig/language-list
  '("emacs-lisp" "python" "ditaa" "plantuml" "shell" "nix"
    "R" "haskell" "latex" "css" "lisp" "jq" "makefile" "go")
  "支持的编程语言列表。")

(defun idiig/run-prog-mode-hooks ()
  "Runs `prog-mode-hook'. 针对一些本该为编程语言又没自动加载prog mode的语言hook.
如:(add-hook 'python-hook 'idiig/run-prog-mode-hooks)
"
  (run-hooks 'prog-mode-hook))

5.8.2. 语言服务器 (lsp-Bridge)

语言服务器用于补全代码,提示文档,参照转跳等。这里我使用了 LSP-Bridge,其主要优势是通过 python 后端调用语言服务器,不卡 emacs 进程达到高速的补全。其他倾向的代替选项有 emacs 捆绑 eglot

  1. 依赖
    (lsp-bridge.override {
      # 指定使用 Python 3.11 而不是 3.12
      python3 = pkgs.python311;
    })
      markdown-mode
      yasnippet
    

    这里由于默认的 python (3.12.9) 版本问题导致了下面的报错:

    • 首先尝试导入 SimpleXMLRPCServer 模块失败,这是因为在 Python 3 中,该模块已被移至 xmlrpc.server
    • 随后在导入 xmlrpc.client 时出现了一个奇怪的错误: 'datetime.datetime' object has no attribute 'task'

    这可能是 Python 3.12.9 中的一个 bug,或者是 epc 包与 Python 3.12.9 不兼容的结果。因此我们在 lsp-bridge 的环境中使用了 311 的版本。

    Traceback (most recent call last):
      File "/nix/store/1bn994va1akp3m0jvg4fj9wzlqmn1kkq-python3-3.12.9-env/lib/python3.12/site-packages/epc/py3compat.py", line 26, in <module>
        import SimpleXMLRPCServer
    ModuleNotFoundError: No module named 'SimpleXMLRPCServer'
    
    During handling of the above exception, another exception occurred:
    
    Traceback (most recent call last):
      File "/nix/store/4mx09lzrlahhkgv7qb2q57xmnsfwcmlx-emacs-packages-deps/share/emacs/site-lisp/elpa/lsp-bridge-20250210.0/lsp_bridge.py", line 46, in <module>
        from epc.server import ThreadingEPCServer
      File "/nix/store/1bn994va1akp3m0jvg4fj9wzlqmn1kkq-python3-3.12.9-env/lib/python3.12/site-packages/epc/server.py", line 20, in <module>
        from .py3compat import SocketServer
      File "/nix/store/1bn994va1akp3m0jvg4fj9wzlqmn1kkq-python3-3.12.9-env/lib/python3.12/site-packages/epc/py3compat.py", line 28, in <module>
        import xmlrpc.server as SimpleXMLRPCServer
      File "/nix/store/26yi95240650jxp5dj78xzch70i1kzlz-python3-3.12.9/lib/python3.12/xmlrpc/server.py", line 107, in <module>
        from xmlrpc.client import Fault, dumps, loads, gzip_encode, gzip_decode
      File "/nix/store/26yi95240650jxp5dj78xzch70i1kzlz-python3-3.12.9/lib/python3.12/xmlrpc/client.py", line 272, in <module>
        if _try('%Y'):      # Mac OS X
           ^^^^^^^^^^
      File "/nix/store/26yi95240650jxp5dj78xzch70i1kzlz-python3-3.12.9/lib/python3.12/xmlrpc/client.py", line 269, in _try
        return _day0.strftime(fmt) == '0001'
               ^^^^^^^^^^^^^^^^^^^
    AttributeError: 'datetime.datetime' object has no attribute 'task'
    
  2. 配置
    1. 后续的设置宏,用于配置 Nix 环境下的 LSP 服务器
      (defvar idiig/lsp-extra-paths nil
        "Emacs 侧已配置的 LSP 可执行目录清单。会被写入到项目的 .emacs-lsp-paths。")
      
      (defmacro idiig//setup-nix-lsp-bridge-server (language server-name executable-path &optional lib-path)
        "配置 Nix 环境下的 LSP 服务器。
      LANGUAGE 是语言名称,如 'python'。
      SERVER-NAME 是服务器名称,如 'basedpyright'。
      EXECUTABLE-PATH 是服务器可执行文件的路径。
      LIB-PATH 是可选的库路径,添加到 LD_LIBRARY_PATH。"
        `(with-eval-after-load 'lsp-bridge
           ;; 设置 LSP 服务器
           (setq ,(intern (format "lsp-bridge-%s-lsp-server" language)) ,server-name)
      
           ;; 添加可执行文件路径到 exec-path
           ,(when executable-path
              `(progn
                 (add-to-list 'exec-path ,executable-path)
                 (add-to-list 'idiig/lsp-extra-paths ,executable-path)))
      
           ;; 添加库路径到 LD_LIBRARY_PATH
           ,(when lib-path
              `(setenv "LD_LIBRARY_PATH" 
                       (concat ,lib-path ":" 
                               (or (getenv "LD_LIBRARY_PATH") ""))))))
      

      上面这个宏用于生成 LSP-Bridge 的设定,因为语言服务器本身需要用户自己安装,而本配置又需要 Nix 保证复现,所以我们需要把 nix 路径的语言服务器传递给 emacs ,而且语言服务器往往需要匹配的 C++ 的库。因此我希望通过上面的代码生成系列配置,同时设定语言,语言服务器,语言服务器的可执行文件路径,依赖的 C 库。宏使用如下:

      (idiig//setup-nix-lsp-bridge-server 
       "python" 
       "basedpyright" 
       "${pkgs.basedpyright}/bin" 
       "${pkgs.stdenv.cc.cc.lib}/lib")
      

      宏展开后如下:

      (with-eval-after-load 'lsp-bridge
        (setq lsp-bridge-python-lsp-bridge-server "basedpyright")
        (add-to-list 'exec-path "${pkgs.basedpyright}/bin")
        (setenv "LD_LIBRARY_PATH" 
                (concat "${pkgs.stdenv.cc.cc.lib}/lib:" 
                        (or (getenv "LD_LIBRARY_PATH") ""))))
      

      后面是 LSP-Bridge 本体的配置:

      (use-package lsp-bridge
        :defer t
        :diminish lsp-bridge-mode
        :bind
        (:map acm-mode-map
              ("C-j" . acm-select-next)
              ("C-k" . acm-select-prev))
        :custom
        (acm-enable-yas nil)   ; 补全不包括 Yasnippet
        (acm-enable-doc nil)   ; 不自动显示函数等文档
        (lsp-bridge-org-babel-lang-list idiig/language-list)  ; org支持的代码也使用桥
        (acm-enable-icon nil)  ; 不显示图标
        :hook 
        (prog-mode . (lambda ()
                       (lsp-bridge-mode)))
        :init
        ;; 这里是为了让语言服务器找到正确的版本的 libstdc++.so.6 库
        (setenv "LD_LIBRARY_PATH" 
                (concat "${pkgs.stdenv.cc.cc.lib}/lib:" 
                        (or (getenv "LD_LIBRARY_PATH") ""))))
      

      注意 :语言服务器不会自动安装,如果在新电脑中出现缺少个别语言服务器的情况,我们可以手动安装。

    2. 弹窗补全优先设置
      (with-eval-after-load 'lsp-bridge
        (defun idiig/acm-prefer-lsp-all ()
          (when (bound-and-true-p lsp-bridge-mode)
            ;; 让 search 后端慢一点再来(避免覆盖 LSP)
            (when (boundp 'acm-backend-search-delay)
              (setq-local acm-backend-search-delay 0.8))  ;; 你可调成 0.6~1.0
      
            ;; 提高 LSP 优先级,降低 search 优先级(若有这些变量)
            (when (boundp 'acm-backend-lsp-priority)
              (setq-local acm-backend-lsp-priority 100))
            (when (boundp 'acm-backend-search-priority)
              (setq-local acm-backend-search-priority 0))
      
            ;; 可选:减少噪声(若存在这些开关)
            (when (boundp 'acm-enable-dabbrev)
              (setq-local acm-enable-dabbrev nil))          ; 关闭 dabbrev 后端
            (when (boundp 'acm-backend-search-candidates-min-length)
              (setq-local acm-backend-search-candidates-min-length 3)))) ; 至少 3 字符再搜
      
        (add-hook 'lsp-bridge-mode-hook #'idiig/acm-prefer-lsp-all))
      

5.8.3. TODO Treesitter

用于解析语法和语法高亮

  1. 依赖
    # treesit  # 目前 treesit 已经内置
    treesit-auto
    
  2. 配置
    (use-package treesit-auto
      :custom
      (treesit-auto-install 'prompt)   ; 设置安装 tree-sitter 语法时提示用户确认
      :hook
      (prog-mode . treesit-auto-mode)    ; 在所有编程模式下自动启用 treesit-auto-mode
      :config
      (treesit-auto-add-to-auto-mode-alist 'all))  ; 将所有已知的 tree-sitter 模式添加到自动模式列表中
    

    这个配置设置了 treesit-auto 包,这是一个帮助管理和自动启用 Emacs 内置 tree-sitter 模式的工具。会在启动 major mode 的时候自动替换为 <major>-ts-mode 。比如 python-mode 会变成 python-ts-mode。

    注意 :treesit 不会自动安装,如果在新电脑中出现缺少 treesit 的情况,我们可以手动 treesit-auto-install-all 。下载目前所有可能的语言的 treesit。

5.8.4. Snippet (yasnippet)

Snippet 用于快速插入模板

  1. 依赖

    这里 yasnippet 本体已经作为 lsp-bridge 的依赖被加入,我们这里只加入一个 snippet 的合集 yasnippert-snippets

    # yasnippet
    yasnippet-snippets
      consult-yasnippet
    
  2. 配置
    ;; (defvar idiig/snippet-dir (concat user-emacs-directory "snippets"))
    (use-package yasnippet
      :defer t
      :diminish yas-minor-mode
      :hook
      (prog-mode . yas-minor-mode)
      :init
      ;; (setq yas-snippet-dirs <path/to/snippets>)
      ;; (push idiig/snippet-dir yas-snippet-dirs)
      :config
      (yas-reload-all))
    

    加载用于yasnippet的的合集

    (use-package consult-yasnippet
      :after
      (consult
       yas-minor-mode))
    

5.8.5. 基于 tresitter 的结构化编程

  1. 依赖
    
    
  2. 配置
    
    

5.8.6. 自动化环境管理 (direnv)

  1. 依赖
    direnv
    
  2. 配置

    启用 direnv-mode 使用 flake 定环境变量。

    (use-package direnv
      :defer t
      :init
      (add-to-list 'exec-path "${pkgs.direnv}/bin")
      :config
      (direnv-mode))
    

    但是项目中的环境变量往往会修改PATH,导致 LSP 等工具找不到可执行文件。而我并不想在flake里写LSP相关的配置,因为倭人为LSP是为了了补全和跳转,而不是为了运行程序和复现环境。因此我们需要把 Emacs 的 exec-path 也合并进 PATH 里,供子进程继承。

    (require 'cl-lib)
    
    (defun idiig/project-root ()
      "返回当前 buffer 对应的项目根(优先含 .envrc,其次 .git)。"
      (or (locate-dominating-file default-directory ".envrc")
          (locate-dominating-file default-directory ".git")
          default-directory))
    
    (defun idiig/write-emacs-lsp-paths ()
      "将 `idiig/lsp-extra-paths` 写入项目根的 .emacs-lsp-paths。"
      (interactive)
      (when-let* ((root (idiig/project-root))
                  (file (expand-file-name ".emacs-lsp-paths" root)))
        (let* ((dirs (->> idiig/lsp-extra-paths
                          (seq-filter #'file-directory-p)
                          (delete-dups))))
          (when dirs
            (with-temp-file file
              (dolist (p dirs) (insert p "\n")))))))
    
    ;; lsp-bridge 项目根识别(避免偶发 no views)
    ;; direnv 集成:allow/refresh 前写清单;完成后自动重启 lsp-bridge
    (with-eval-after-load 'direnv
      ;; before:生成/更新 .emacs-lsp-paths,供 .envrc 读取
      (advice-add 'direnv-allow :before (lambda (&rest _) (idiig/write-emacs-lsp-paths)))
      (when (fboundp 'direnv-update-environment)
        (advice-add 'direnv-update-environment :before
                    (lambda (&rest _) (idiig/write-emacs-lsp-paths))))
    
      ;; after:环境就绪后,如有需要自动重启 lsp-bridge
      (defun idiig/direnv--restart-lsp-bridge (&rest _)
        (when (and (featurep 'lsp-bridge)
                   (fboundp 'lsp-bridge-restart-process)
                   (cl-some (lambda (buf)
                              (with-current-buffer buf
                                (bound-and-true-p lsp-bridge-mode)))
                            (buffer-list)))
          (lsp-bridge-restart-process)))
      (advice-add 'direnv-allow :after #'idiig/direnv--restart-lsp-bridge)
      (when (fboundp 'direnv-update-environment)
        (advice-add 'direnv-update-environment :after #'idiig/direnv--restart-lsp-bridge)))
    

5.8.7. 终端 (eat)

  1. 依赖
    eat
    
  2. 配置
    ;; For `eat-eshell-mode'.
    (add-hook 'eshell-load-hook #'eat-eshell-mode)
    
    ;; For `eat-eshell-visual-command-mode'.
    (add-hook 'eshell-load-hook #'eat-eshell-visual-command-mode)
    

5.9. 编程与文档语言

5.9.1. Nix

  1. 依赖
    nix-mode
    

    加入 nix 的 major mode。

    (idiig//setup-nix-lsp-bridge-server 
     "nix" 
     "nixd" 
     "${pkgs.nixd}/bin" 
     nil)
    

    设置 nix 的语言服务器。注意这里使用的是在 LSP-Bridge 节中写的宏(参看 5.8.2)。

  2. 配置

5.9.2. Lisp

  1. RERL
    1. 依赖
      slime
        geiser                        # for scheme
      
    2. 配置
      (use-package slime
        :init
        (setq inferior-lisp-program
              (or (executable-find "sbcl")
                  "${pkgs.sbcl}/bin/sbcl"))
        :config
        (slime-setup '(slime-fancy)))
      
  2. 方言
    1. Elisp
      1. 配置
        • M-: 时的 eval expression minibuffer 的时候加入 prog mode。
        (add-hook 'eval-expression-minibuffer-setup 'idiig/run-prog-mode-hooks)
        
    2. Clojure
      1. 依赖
        (idiig//setup-nix-lsp-bridge-server 
         "clojure" 				; language name
         "clojure-lsp" 				; lsp name
         "${pkgs.clojure-lsp}/bin"		; dependency nixpkg path
         nil)					; other dependencies
        

5.9.3. Python

  1. 本体
    1. 依赖
      (idiig//setup-nix-lsp-bridge-server 
       "python" 
       "basedpyright" 
       "${pkgs.basedpyright}/bin" 
       "${pkgs.stdenv.cc.cc.lib}/lib")
      
    2. 配置
  2. 虚拟环境

5.9.4. Haskell

  1. 本体
    1. 依赖

      Major mode:

      haskell-mode
      
      (idiig//setup-nix-lsp-bridge-server 
       "haskell"
       "hls" 
       "${pkgs.haskell-language-server}/bin" 
       nil)
      

5.9.5. R

5.9.6. Go

  1. 本体
    1. 依赖

      Major mode:

      go-mode
      

      LSP:

      (idiig//setup-nix-lsp-bridge-server 
       "go" 
       "gopls" 
       "${pkgs.gopls}/bin" 
       nil)
      
    2. 配置
      (defun idiig/go-prefer-lsp ()
        (when (derived-mode-p 'go-mode 'go-ts-mode)
          ;; 关闭文件内/跨缓冲词搜索后端(如果你的版本有这些开关)
          (when (boundp 'acm-enable-search-file-words)
            (setq-local acm-enable-search-file-words nil))
          (when (boundp 'acm-enable-dabbrev)
            (setq-local acm-enable-dabbrev nil))
          ;; 把搜索词的延迟调大,避免覆盖(若有这个变量)
          (when (boundp 'acm-backend-search-delay)
            (setq-local acm-backend-search-delay 0.8))
          ;; LSP 候选最短前缀更短一些(若有)
          (when (boundp 'acm-backend-lsp-candidate-min-length)
            (setq-local acm-backend-lsp-candidate-min-length 0))))
      (add-hook 'go-mode-hook #'idiig/go-prefer-lsp)
      (add-hook 'go-ts-mode-hook #'idiig/go-prefer-lsp)
      

5.9.7. Shell

  1. 依赖
    (idiig//setup-nix-lsp-bridge-server 
     "bash" 				; language name
     "bash-language-server" 		; lsp name
     "${pkgs.bash-language-server}/bin" 	; dependency nixpkg path
     nil)					; other dependencies
    
  2. 配置
    1. bash 作为默认 shell
      ;; (setq shell-command-switch "-ic")
      (setq-default explicit-shell-file-name "${pkgs.bashInteractive
      }/bin/bash")
      (setq shell-file-name "${pkgs.bashInteractive
      }/bin/bash")
      

5.9.8. jq

jq-mode

5.9.9. Make

5.9.10. TeX

  1. 依赖
    auctex
      auctex-latexmk
    
    (idiig//setup-nix-lsp-bridge-server 
     "tex" 
     "texlab" 
     "${pkgs.texlab}/bin" 
     nil)
    
  2. 配置
    (add-hook 'TeX-mode-hook 'idiig/run-prog-mode-hooks)
    
    (use-package auctex
      :defer t)
    

5.9.11. Typst

5.9.12. Markdown

5.9.13. Quarto

5.9.14. XML

5.9.15. JSON

  1. 依赖
    jsonian
      json-mode
    
    (idiig//setup-nix-lsp-bridge-server 
     "json" 
     "vscode-json-language-server" 
     "${pkgs.vscode-langservers-extracted}/bin" 
     nil)
    
  2. 配置
    (use-package jsonian
      :after so-long
      :custom
      (jsonian-no-so-long-mode))
    

5.9.16. Web

5.9.17. Java

  1. 依赖
    (pkgs.jre_minimal)
    

5.9.18. PlantUML

  1. 依赖
    plantuml-mode
    
    (add-to-list 'exec-path "${pkgs.plantuml}/bin")
    (with-eval-after-load 'org
      (setq org-plantuml-jar-path "${pkgs.plantuml}/lib/plantuml.jar")
      (setq org-plantuml-executable-path "${pkgs.plantuml}/bin/plantuml")
      (setq org-plantuml-exec-mode 'plantuml))
    

5.9.19. GraphViz

  1. 依赖
    (add-to-list 'exec-path "${pkgs.graphviz}/bin")
    

5.10. Org相关配置

5.10.1. Org本体

  1. 绑定 prog mode
    (add-hook 'org-mode-hook 'idiig/run-prog-mode-hooks)
    
  2. 代码块支持语言
    1. 依赖

      由于 ob-nix, ob-go 还没有默认。我们需要添加这些依赖

      ob-nix
        ob-go
      
    2. 配置
      (defun idiig/load-org-babel-languages ()
        "根据 `idiig/language-list` 启用 `org-babel` 语言。"
        (let ((languages '()))
          (dolist (lang idiig/language-list)
            (push (cons (intern lang) t) languages)) ;; 将字符串转换为符号
          (org-babel-do-load-languages 'org-babel-load-languages languages)))
      
      (defun idiig/set-org-babel-language-commands ()
        "根据 `idiig/language-list` 甚至语言的命令。"
        (dolist (lang idiig/language-list)
          (let ((var-name (intern (format "org-babel-%s-command" lang))))
            (when (boundp var-name)
              (set var-name (executable-find lang))))))
      
      (add-hook 'org-mode-hook #'idiig/load-org-babel-languages)
      (add-hook 'org-mode-hook #'idiig/set-org-babel-language-commands)
      
      ;; 特殊
      (setq org-babel-shell-command (executable-find "bash"))
      
  3. 结构化插入Babel模板

    org-insert-structure-template 后选择 s (src block) 时自动提示插入代码块的语言。

    更改这个函数:

    • 新增一个功能,如果输入的语言不在列表里,则把语言加入列表。但仅限于当前org文档
    • 新增一个功能,根据输入的语言更新推荐语言的排序。
    (with-eval-after-load 'org
      (defun idiig/org-insert-structure-template-src-advice (orig-fun type)
        "Advice for org-insert-structure-template to handle src blocks."
        (if (string= type "src")  ; 判断条件为 "src"
            (let ((selected-type (ido-completing-read "Source code type: " idiig/language-list)))
              (funcall orig-fun (format "src %s" selected-type)))
          (funcall orig-fun type)))
    
      (advice-add 'org-insert-structure-template :around #'idiig/org-insert-structure-template-src-advice))
    
  4. 基础设定
    1. 默认进入 org-mode 时的设定
      • 显示图片
      • 显示 LaTeX 公式
      • 折叠显示所有顶层节点
      • 展开显示当前节点内容
      • 展开显示所有子节点但不展开下一级
      (with-eval-after-load 'org
        (setq org-startup-with-inline-images t) ; 启动时显示图片
        (setq org-startup-with-latex-preview t) ; 启动时显示 LaTeX 公式
        (add-hook 'org-mode-hook
                  (lambda ()
                    (org-overview)		; 显示所有顶层节点
                    (org-show-entry)		; 显示当前节点内容
                    (org-show-children))))	; 显示所有子节点但不展开
      
    2. 允许shift用于选择
      (with-eval-after-load 'org
        (setq org-support-shift-select 2))
      
    3. 远程图片文件可以通过 C-u C-c C-x C-v 被看到
      (with-eval-after-load 'org
        (setq org-display-remote-inline-images t))
      
    4. 允许 #+bind 关键词
      (with-eval-after-load 'org
        (setq org-export-allow-bind-keywords t))
      
  5. 面貌
    1. 基础美化
      (with-eval-after-load 'org
       ;; Edit settings
       (setq org-auto-align-tags nil		; 禁用标签自动对齐功能
             org-tags-column 0		; 标签紧贴标题文本,不右对齐
             org-catch-invisible-edits 'show-and-error  ; 编辑折叠内容时显示并报错提醒
             org-special-ctrl-a/e t ; 增强 C-a/C-e,先跳到内容开始/结束,再跳到行首/尾
             org-insert-heading-respect-content t ; 插入标题时考虑内容结构,在内容后插入
      
             ;; Org styling, hide markup etc.
             org-hide-emphasis-markers t ; 隐藏强调标记符号 (*粗体* 显示为 粗体)
             org-pretty-entities t))	  ; 美化显示实体字符 (\alpha 显示为 α)
      
    2. 字体,缩进,换行设定
      (defun idiig/org-mode-face-settings ()
        "Set custom face attributes for Org mode headings in current buffer only."
      
        (auto-fill-mode 0)			; Disable auto-fill mode
        (require 'org-indent)			; Ensure org-indent is loaded
        (org-indent-mode)			; Enable org-indent mode
        (variable-pitch-mode 1)		; Enable variable-pitch mode
        (visual-line-mode 1)			; Enable visual-line mode for soft wrapping
      
        (defvar idiig/fixed-width-font "Sarasa Mono J"
          "The font to use for monospaced (fixed width) text.")
      
        (defvar idiig/variable-width-font "Sarasa Gothic J"
          "The font to use for variable-pitch (document) text.")
      
        (set-face-attribute 'default nil
                            :font idiig/fixed-width-font
                            :weight 'regular
                            :height 160)
        (set-face-attribute 'fixed-pitch nil
                            :font idiig/fixed-width-font
                            :weight 'regular
                            :height 170)
        (set-face-attribute 'variable-pitch nil
                            :font idiig/variable-width-font
                            :weight 'regular
                            :height 1.3)
        (buffer-face-set `(:family ,idiig/fixed-width-font
                                   :height 1.1)) ; Set buffer face
      
        (setq-local line-spacing 0.3)		; Set line spacing
      
        (let ((faces '((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))))
          (dolist (face faces)
            `(face-remap-add-relative (car face)
                                     :family ,idiig/variable-width-font
                                     :weight 'regular
                                     :height (cdr face))))
      
        ;; Make sure certain org faces use the fixed-pitch face when variable-pitch-mode is on
        (set-face-attribute 'org-block nil :foreground nil :inherit 'fixed-pitch)
        (set-face-attribute 'org-table nil :inherit 'fixed-pitch)
        (set-face-attribute 'org-formula nil :inherit 'fixed-pitch)
        (set-face-attribute 'org-code nil :inherit '(shadow fixed-pitch))
        (set-face-attribute 'org-verbatim nil :inherit '(shadow fixed-pitch))
        (set-face-attribute 'org-special-keyword nil :inherit '(font-lock-comment-face fixed-pitch))
        (set-face-attribute 'org-meta-line nil :inherit '(font-lock-comment-face fixed-pitch))
        (set-face-attribute 'org-checkbox nil :inherit 'fixed-pitch)
      
        ;; Make the document title a bit bigger
        (set-face-attribute 'org-document-title nil :font idiig/variable-width-font :weight 'bold :height 1.3)
      
        (with-eval-after-load 'diminish
          (diminish 'org-indent-mode)
          (diminish 'buffer-face-mode)))
      
      (add-hook 'org-mode-hook 'idiig/org-mode-face-settings)
      
    3. 数学渲染
      (with-eval-after-load 'org
        (add-to-list
         'org-preview-latex-process-alist
         '(idiig-dvisvgm
           :programs ("${pkgs.texliveMedium}/bin/latex" "${pkgs.texliveMedium}/bin/dvisvgm")
           :description "latex -> dvi -> svg (nix-store)"
           :message "use latex and dvisvgm from nix-store."
           :image-input-type "dvi"
           :image-output-type "svg"
           :image-size-adjust (0.8 . 1.0)
           :latex-compiler ("${pkgs.texliveMedium}/bin/latex -interaction nonstopmode -output-directory %o %f")
           :image-converter ("${pkgs.texliveMedium}/bin/dvisvgm %f --no-fonts --exact-bbox --scale=%S --output=%O")))
      
        (setq org-latex-create-formula-image-program 'idiig-dvisvgm))
      
    4. Bullets
      1. 依赖
        org-bullets
        
      2. 配置
        (use-package org-bullets
          :after org
          :hook (org-mode . org-bullets-mode)
          :custom
          (org-bullets-bullet-list '("◉" "○" "●" "○" "●" "○" "●")))
        
        (font-lock-add-keywords 'org-mode
                                '(("^ *\\([-]\\) "
                                   (0 (prog1 () (compose-region (match-beginning 1) (match-end 1) "•"))))))
        
        (with-eval-after-load 'org
          (setq org-ellipsis " ▾"
                org-hide-emphasis-markers t))
        

5.10.2. 文献引用处理器 (citeproc-el)

  1. 依赖
    citeproc
    
  2. 配置
    (with-eval-after-load 'org
      (setq org-cite-export-processors
          '((latex biblatex)
            (html csl)
            (odt  csl)
            (t    biblatex))))
    

5.10.3. 出入引用配置 (oc, oc-basic)

org-cite 增强:插入后选择样式和参数。

  1. 配置
    (with-eval-after-load 'oc
      (require 'oc-basic)
      (require 'subr-x)
    
      ;; 样式选择和参数输入
      (defun my/oc--ask-style-and-affixes ()
        "询问 citation 样式和附加参数。"
        (let* ((style-help "
    Style examples:
      cite/a/cf → Citeauthor – Aa, Bb, and Cc
      cite/a/c  → Citeauthor – Aa et al.
      cite/na   → citeyear – 2022
      cite/na/b → citeyearpar – (2022)
      cite/t/c  → Citet – Aa et al. (2022)
      cite/t/cf → citep* – Aa, Bb, and Cc (2022)
      cite/bc   → Cite – Aa et al. 2022
      cite/cf   → Citep – (Aa et al. 2022)")
               (presets '(("cite/a/cf" . ("a"  . "cf"))
                          ("cite/a/c"  . ("a"  . "c"))
                          ("cite/na"   . ("na" . ""))
                          ("cite/na/b" . ("na" . "b"))
                          ("cite/t/c"  . ("t"  . "c"))
                          ("cite/t/cf" . ("t"  . "cf"))
                          ("cite/bc"   . ("bc" . ""))
                          ("cite/cf"   . ("cf" . ""))))
               (choice (completing-read
                        "Citation style (? for help): "
                        (append (mapcar #'car presets) '("custom" "?"))
                        nil t nil nil "cite/cf"))
               (l "") (b ""))
    
          ;; 显示帮助
          (when (string= choice "?")
            (message "%s" style-help)
            (sit-for 3)
            (setq choice (completing-read "Citation style: "
                                          (append (mapcar #'car presets) '("custom"))
                                          nil t nil nil "cite/cf")))
    
          ;; 获取样式
          (if (not (string= choice "custom"))
              (let ((p (cdr (assoc choice presets))))
                (setq l (car p) b (cdr p)))
            (setq l (completing-read "Style (l): " '("a" "na" "t" "bc" "c" "cf" "") nil t ""))
            (setq b (completing-read "Variant (b): " '("b" "c" "cf" "") nil t "")))
    
          ;; 构建完整样式和参数
          (let* ((style (string-join (seq-filter (lambda (x) (and x (not (string-empty-p x))))
                                                 (list l b))
                                     "/"))
                 (prefix  (read-string "Prefix (optional): "))
                 (locator (read-string "Locator (optional): "))
                 (suffix  (read-string "Suffix (optional): ")))
            (list style prefix locator suffix))))
    
      ;; 修改插入点的 citation
      (defun my/oc--rewrite-cite-at-point (style prefix locator suffix)
        "在当前位置修改 citation 的样式和参数。"
        (save-excursion
          (let* ((ctx (org-element-context))
                 (cite (if (eq (org-element-type ctx) 'citation)
                           ctx
                         (when (re-search-backward "\\[cite\\>" (max (point-min) (- (point) 1000)) t)
                           (org-with-point-at (match-beginning 0)
                             (org-element-citation-parser))))))
            (when cite
              (let* ((beg (org-element-begin cite))
                     (end (org-element-end   cite))
                     (has-style (save-excursion (goto-char beg) (looking-at-p "\\[cite/")))
                     (has-colon (save-excursion
                                  (goto-char beg)
                                  (re-search-forward "\\[cite\\(?:/[^:]]+\\)?\\(:\\)" end t))))
    
                ;; 添加样式
                (when (and (not has-style) (not (string-empty-p style)))
                  (goto-char (+ beg 5))
                  (insert "/" style)
                  (setq end (+ end (length style) 1)))
    
                ;; 添加前缀
                (when (not (string-empty-p prefix))
                  (unless has-colon
                    (goto-char beg)
                    (re-search-forward "\\[cite\\(?:/[^:]]+\\)?" end t)
                    (insert ":")
                    (setq end (1+ end)))
                  (goto-char beg)
                  (re-search-forward ":" end t)
                  (insert prefix " ")
                  (setq end (+ end (length prefix) 1)))
    
                ;; 添加定位符
                (when (not (string-empty-p locator))
                  (goto-char beg)
                  (when (re-search-forward "@[^; \t\n]+" end t)
                    (insert " " locator)
                    (setq end (+ end (length locator) 1))))
    
                ;; 添加后缀
                (when (not (string-empty-p suffix))
                  (goto-char (1- end))
                  (insert " " suffix)))))))
    
      ;; 主 advice:插入后弹出二次对话
      (defun my/oc-insert-then-ask (orig-fn arg)
        "插入引用后询问样式和参数。"
        (let ((ret (funcall orig-fn arg)))
          (run-at-time
           0 nil
           (lambda ()
             (condition-case err
                 (pcase-let ((`(,style ,prefix ,locator ,suffix)
                              (my/oc--ask-style-and-affixes)))
                   (my/oc--rewrite-cite-at-point style prefix locator suffix))
               (quit  (message "Citation style canceled"))
               (error (message "Error setting citation style: %s" (error-message-string err))))))
          ret))
    
      ;; 挂载 advice
      (advice-add 'org-cite-insert :around #'my/oc-insert-then-ask))
    

5.10.4. 幻灯片 (org-present)

org-present 作幻灯片发表。手动开启。

  1. 依赖
    org-present
    

5.11. AI辅助功能

5.11.1. Copilot

  1. 依赖
    copilot
    
  2. 配置
    (use-package copilot
      :hook (prog-mode . copilot-mode)
      :config
      (define-key copilot-completion-map (kbd "<tab>") 'copilot-accept-completion)
      (add-to-list 'copilot-indentation-alist '(prog-mode 2))
      (add-to-list 'copilot-indentation-alist '(org-mode 2))
      (add-to-list 'copilot-indentation-alist '(text-mode 2))
      (add-to-list 'copilot-indentation-alist '(lisp-mode 2))
      (add-to-list 'copilot-indentation-alist '(emacs-lisp-mode 2))
    
      (setq copilot-max-char 99999999))
    

5.11.2. Chatbot (gptel)

  1. 依赖
    gptel
    
  2. 配置
    (add-hook 'org-mode-hook
              (lambda ()
                (when (string-match-p "\\.ai\\.org\\'" (buffer-file-name))
                  (gptel-mode 1))))
    

5.11.3. AI-pairing programming (Aidermacs)

  1. 依赖
    1. aidermacs.el
      aidermacs
      
    2. Aider 目录路径
      (add-to-list 'exec-path "${pkgs.aider-chat}/bin")
      
  2. 配置

    Aidermacs 是一个用于在 Emacs 中使用 Aider 的 AI 辅助编程工具,集成了多种 AI 模型服务,帮助用户在编程过程中获得智能建议和辅助。

    1. API Key 环境变量

      它需要 API Key 来使用 OpenAI 或者其他模型服务。目前我主要支持的模型服务提供商有 OpenAI、Anthropic 和 Google。

      (defvar idiig/supported-providers '("openai" "anthropic" "google")
        "List of supported AI providers for Aider.")
      
      (defun idiig/provider-env-var (provider)
        "Return the environment variable name for the given PROVIDER.
      Uppercase the provider name and append '_API_KEY'."
        (let ((provider-lower (downcase provider)))
          (if (member provider-lower idiig/supported-providers)
              (concat (upcase provider-lower) "_API_KEY")
            (error "Unsupported provider: %s" provider))))
      
      (defun idiig/api-path (provider dir)
        "Return the file path for the API key of PROVIDER in directory DIR."
        (expand-file-name provider dir))
      
      (defun idiig/read-file-contents (file-path)
        "Return the contents of FILE-PATH as a string, with error handling."
        (condition-case err
            (if (file-exists-p file-path)
                (string-trim (with-temp-buffer
                               (insert-file-contents file-path)
                               (buffer-string)))
              (error "File does not exist: %s" file-path))
          (error
           (message "Error reading file %s: %s" file-path (error-message-string err))
           nil)))
      
      (defun idiig/setup-single-provider (provider api-dir)
        "Set up API key for a single PROVIDER from API-DIR.
      Return t on success, nil on failure."
        (let* ((provider-env (idiig/provider-env-var provider))
               (provider-path (idiig/api-path provider api-dir))
               (api-key (idiig/read-file-contents provider-path)))
          (if api-key
              (progn
                (setenv provider-env api-key)
                (message "Set %s from %s" provider-env provider-path)
                t)
            (progn
              (message "Failed to read API key for %s from %s" provider provider-path)
              nil))))
      
      (defun idiig/get-default-api-dir ()
        "Get the default API directory.
      Check if 'api-key' or 'api-keys' folder exists in current directory.
      Return the path if found, otherwise return current directory."
        (let ((current-dir default-directory)
              (possible-dirs '("api-key" "api-keys")))
          (or (seq-find (lambda (dir)
                          (let ((full-path (expand-file-name dir current-dir)))
                            (and (file-directory-p full-path) full-path)))
                        possible-dirs)
              ;; If none found, query user for directory using `read-directory-name`
              (read-directory-name "Select API keys directory: " current-dir nil t))))
      
      (defun idiig/setup-api-keys (&optional api-dir)
        "Set up API keys for Aider from directory containing API files.
      If API-DIR is provided, use it directly. Otherwise, check if current
      folder has 'api-key' or 'api-keys' folder and use it as default.
      Interactively prompts for the directory with smart default."
        (interactive)
        (let* ((default-dir (idiig/get-default-api-dir))
               (prompt (if (string= default-dir default-directory)
                           "Select API keys directory: "
                         (format "Select API keys directory (default: %s): " 
                                 (file-name-nondirectory (directory-file-name default-dir)))))
               (selected-dir (if (called-interactively-p 'any)
                                 (read-directory-name prompt default-dir)
                               (or api-dir default-dir)))
               (results (mapcar (lambda (provider)
                                  (idiig/setup-single-provider provider selected-dir))
                                idiig/supported-providers))
               (success-count (length (seq-filter #'identity results)))
               (total-count (length idiig/supported-providers)))
      
          ;; Provide comprehensive feedback
          (if (= success-count total-count)
              (message "Successfully set all %d API keys from directory: %s" 
                       total-count selected-dir)
            (message "Set %d of %d API keys from directory: %s (check messages for details)"
                     success-count total-count selected-dir))
      
          ;; Return success status for programmatic use
          (= success-count total-count)))
      
    2. Aidermacs 配置

      启用 C-c a 快捷键打开菜单。默认使用 sonnet 模型。

      (use-package aidermacs
        :bind (("C-c a" . aidermacs-transient-menu))
        :config
        (idiig/setup-api-keys)
        :custom
        (aidermacs-default-chat-mode 'architect)
        (aidermacs-default-model "sonnet"))
      

5.11.4. TODO AI-Client (claude-code)

  1. 依赖
    claude-code
    
  2. 配置
    ;; Add to your init.el
    (use-package claude-code
      :init
      (add-to-list 'exec-path "${pkgs.claude-code}/bin")
      :bind (("C-c c" . claude-code-transient)))
    

5.12. 模态编辑 (meow)

5.12.1. 依赖

meow
  meow-tree-sitter

5.12.2. 配置

(use-package meow
  :init
  ;; https://github.com/meow-edit/meow/blob/master/KEYBINDING_QWERTY.org
  (require 'meow)
  (defun meow-setup ()
    (setq meow-cheatsheet-layout meow-cheatsheet-layout-qwerty)
    (meow-motion-define-key
     '("j" . meow-next)
     '("k" . meow-prev)
     '("<escape>" . ignore))
    (meow-leader-define-key
     ;; Use SPC (0-9) for digit arguments.
     '("1" . meow-digit-argument)
     '("2" . meow-digit-argument)
     '("3" . meow-digit-argument)
     '("4" . meow-digit-argument)
     '("5" . meow-digit-argument)
     '("6" . meow-digit-argument)
     '("7" . meow-digit-argument)
     '("8" . meow-digit-argument)
     '("9" . meow-digit-argument)
     '("0" . meow-digit-argument)
     '("/" . meow-keypad-describe-key)
     '("?" . meow-cheatsheet))
    (meow-normal-define-key
     '("0" . meow-expand-0)
     '("9" . meow-expand-9)
     '("8" . meow-expand-8)
     '("7" . meow-expand-7)
     '("6" . meow-expand-6)
     '("5" . meow-expand-5)
     '("4" . meow-expand-4)
     '("3" . meow-expand-3)
     '("2" . meow-expand-2)
     '("1" . meow-expand-1)
     '("-" . negative-argument)
     '(";" . meow-reverse)
     '("," . meow-inner-of-thing)
     '("." . meow-bounds-of-thing)
     '("[" . meow-beginning-of-thing)
     '("]" . meow-end-of-thing)
     '("a" . meow-append)
     '("A" . meow-open-below)
     '("b" . meow-back-word)
     '("B" . meow-back-symbol)
     '("c" . meow-change)
     '("d" . meow-delete)
     '("D" . meow-backward-delete)
     '("e" . meow-next-word)
     '("E" . meow-next-symbol)
     '("f" . meow-find)
     '("g" . meow-cancel-selection)
     '("G" . meow-grab)
     '("h" . meow-left)
     '("H" . meow-left-expand)
     '("i" . meow-insert)
     '("I" . meow-open-above)
     '("j" . meow-next)
     '("J" . meow-next-expand)
     '("k" . meow-prev)
     '("K" . meow-prev-expand)
     '("l" . meow-right)
     '("L" . meow-right-expand)
     '("m" . meow-join)
     '("n" . meow-search)
     '("o" . meow-block)
     '("O" . meow-to-block)
     '("p" . meow-yank)
     '("q" . meow-quit)
     '("Q" . meow-goto-line)
     '("r" . meow-replace)
     '("R" . meow-swap-grab)
     '("s" . meow-kill)
     '("t" . meow-till)
     '("u" . meow-undo)
     '("U" . meow-undo-in-selection)
     '("v" . meow-visit)
     '("w" . meow-mark-word)
     '("W" . meow-mark-symbol)
     '("x" . meow-line)
     '("X" . meow-goto-line)
     '("y" . meow-save)
     '("Y" . meow-sync-grab)
     '("z" . meow-pop-selection)
     '("'" . repeat)
     '("<escape>" . ignore)))
  (meow-setup)
  :config
  (meow-global-mode 1))

给meow增加treesitter的功能:

(require 'meow-tree-sitter)
(meow-tree-sitter-register-defaults)  

在 meow-edit 退出 insert-state 时,当前输入方式自动被关闭,而再次进入 insert-state 时重新打开输入方式:

(defvar-local the-late-input-method nil)
(add-hook 'meow-insert-enter-hook
          (lambda ()
            (activate-input-method the-late-input-method)))
(add-hook 'meow-insert-exit-hook
          (lambda ()
            (setq the-late-input-method current-input-method)
            (deactivate-input-method)))

5.13. 多线程丰富功能 (EAF)

EAF(Emacs Application Framework)是一个在 Emacs 中运行的应用框架,我偶尔使用这个框架来使用浏览器和 PDF 阅读器等应用。

5.13.1. 依赖

(eaf.withApplications [ eaf-browser eaf-pdf-viewer ])

5.13.2. 配置

(require 'eaf)
(require 'eaf-browser)
(require 'eaf-pdf-viewer)
(add-to-list 'exec-path "${pkgs.wmctrl}/bin")
(setq eaf-webengine-default-zoom 2.0
        eaf-browse-blank-page-url "https://www.kagi.com"
        eaf-browser-auto-import-chrome-cookies nil   ; 非自动 cookies
        eaf-browser-enable-autofill t                ; 自动填充密码
        eaf-browser-enable-tampermonkey t)	     ; 使用油猴

5.14. 特殊扩展

扩展主要针对不存在于 nixpkgs 中的包,这里我基本上使用了这个链接的代码和方法,由于Nix水平有限,我在代码中增加了一些注释提示自己:

{ inputs, pkgs, emacsPackages }: let
  inherit (builtins) readDir;
  inherit (pkgs) runCommand;
  inherit (pkgs.lib) attrNames attrsToList filter functionArgs hasAttr mergeAttrsList pipe readFile remove;
  packagesDir = ./.;
  packageSources = inputs // {
    # nano = inputs.nano-emacs;
  };
  importFile = dir: let
    packageFunction = import "${packagesDir}/${dir}";
  in emacsPackages.callPackage packageFunction (
    pipe ({
      elispFileVersion = file: let
        output = runCommand "${baseNameOf file}-version" { } ''
          ${emacsPackages.emacs}/bin/emacs -Q --batch \
            --eval "(require 'lisp-mnt)" \
            --eval '(setq pkg-version (lm-version "${file}"))' \
            --eval '(find-file (getenv "out"))' \
            --eval '(insert pkg-version)' \
            --eval '(save-buffer)'
        '';
      in readFile output;
      pkgFileVersion = file: let
        output = runCommand "${baseNameOf file}-version" { } ''
          ${emacsPackages.emacs}/bin/emacs -Q --batch \
            --eval '(find-file "${file}")' \
            --eval '(setq pkg-version (caddr (read (current-buffer))))' \
            --eval '(find-file (getenv "out"))' \
            --eval '(insert pkg-version)' \
            --eval '(save-buffer)'
        '';
      in readFile output;
      normalizeVersion = name: version: let
        output = runCommand "${name}-normalized-version" { } ''
          ${emacsPackages.emacs}/bin/emacs -Q --batch \
            --load package \
            --eval '(setq pkg-version (package-version-join (version-to-list "${version}")))' \
            --eval '(find-file (getenv "out"))' \
            --eval '(insert pkg-version)' \
            --eval '(save-buffer)'
        '';
      in readFile output;
      genericBuild = emacsPackages.callPackage "${inputs.nixpkgs}/pkgs/applications/editors/emacs/build-support/generic.nix" { };
      elpa2nix = "${inputs.nixpkgs}/pkgs/applications/editors/emacs/build-support/elpa2nix.el";
      melpa2nix = "${inputs.nixpkgs}/pkgs/applications/editors/emacs/build-support/melpa2nix.el";
    } // (if hasAttr dir packageSources then { package_src = packageSources.${dir}; } else { })
    ) [
      attrsToList
      # => [ 
      #      { name = "elispFileVersion"; value = <function>; }
      #      { name = "pkgFileVersion"; value = <function>; }
      #      { name = "normalizeVersion"; value = <function>; }
      #      { name = "genericBuild"; value = <function>; }
      #      { name = "elpa2nix"; value = "/path/to/elpa2nix.el"; }
      #      { name = "melpa2nix"; value = "/path/to/melpa2nix.el"; }
      #    ]
      (filter ({ name, ... }: hasAttr name (functionArgs packageFunction)))
      # => 假设 packageFunction 需要 elispFileVersion 和 genericBuild
      # => [ 
      #      { name = "elispFileVersion"; value = <function>; }
      #      { name = "genericBuild"; value = <function>; }
      #    ]
      (map ({ name, value }: { ${name} = value; }))
      # => [
      #      { elispFileVersion = <function>; }
      #      { genericBuild = <function>; }
      #    ]
      mergeAttrsList
      # => { 
      #      elispFileVersion = <function>;
      #      genericBuild = <function>;
      #    }
      # 一般来讲,我们会这么写 import XXX.nix { inherit attr; };
      # 这里相当于最后得到一个传入 XXX.nix 的一个参数集
    ]
  );  # Nix 中 pipe 的写法是 pipe <初始对象> [ <函数1> <函数2> ... ]
in pipe packagesDir [                            # => ./. (当前包目录)
  readDir                                        # => { "package1" = "directory"; "package2" = "directory"; "default.nix" = "regular"; ... }
  attrNames                                      # => [ "package1" "package2" "default.nix" ... ]
  (remove "default.nix")                         # => [ "package1" "package2" ... ]
  (map (dir: { "${dir}" = importFile dir; }))    # => [ { "package1" = <derivation>; } { "package2" = <derivation>; } ... ]
  mergeAttrsList                                 # => { "package1" = <derivation>; "package2" = <derivation>; ... }
]

Footnotes:

1

关于文学编程中 noweb 的解释可以参考 这个链接 。本身我们还有一个 :session 的方式更适合一些数据科学的工作流。这里我们主要为了和 :tangle 以前使用。

Author: idiig

Created: 2025-11-04 Tue 02:03

Validate