基于 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 了。
5.2.3. 更好的默认设置
- 本体的设定
;; 关闭警告声 (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)
- 优化长文档 (
so-long)
- UI
- 基础
;; 基础设置 (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)
- 写作和展示UI(手动开启)
- 依赖
spacious-padding writeroom-mode
- 配置
(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)))
- 依赖
- 基础
- 光标跳到新窗口
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))) - 专用buffer(display-buffer行为;主要影响 Emacs 自动创建的窗口(如
help、compilation 等)。注意这里也会影响到
- 窗口的放大缩小转变为持续的行为
而不是要一直要重复
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) - 文件的保存与新建
;; 不存在文档时询问是否新建 (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))
- 文件管理 (
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))
- 便利的光标首尾移动
- 折行与复原
- 更好的minibuffer
- 原生设定
(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) - 基础五件套
- 依赖
vertico orderless marginalia embark consult embark-consult
- 配置
- Vertico: 提供minibuffer补全UI
(use-package vertico :after consult :custom (vertico-count 9) (vertico-cycle t) :init (vertico-mode))
- 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))) - 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)) - 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))) - 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))
- Vertico: 提供minibuffer补全UI
- 依赖
- 原生设定
- 撤销 (
vundo)
我原来使用 undotree ,现在使用 vundo。这些用于视觉化撤销树。这里我之绑定了
C-x u,C-/我依然用的原生的 Undo,这样适合区分使用。 - 检索 (
ctrlf)
针对当前 buffer 利用
Ctrlf而不在使用swiper和helm这类型的检索方式。用于替代isearch的C-s键。另外的选项是consult-line,我映射到了C-c s键位,用于不移动当前位置预览检索行,尤其是在类似与 org 存在折叠的情况下,我不需要移动光标和展开折叠。关于consult,可见Consult: 增强 minibuffer的检索。- 依赖
ctrlf
- 配置
- 启动
ctrf
(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))
- 启动
- 依赖
- 重构 (
wgrep)
在
grep,ag,ripgrep等检索的结果中按下e进入编辑模式,按下C-c C-c完成修改。 - 括号匹配
(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)) - 语言无关的结构化编程 (
puni)
- 依赖
puni
- 配置
(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))) )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)))))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))
- 依赖
- 覆盖
<Backspace>和<DEL>等删除动作
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)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)- 检查光标后的字符:
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)
- 依赖
ddskk
- 配置
(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)
- 依赖
migemo
注意这里使用的是
cmigemo。 - 配置
- 基础配置
(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)) - 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))))) - 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)
- 依赖
pyim pyim-basedict
- 配置
- 基础配置
(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))
- 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)) - 在minibuffer中用
- buffer内检索 (
Ctrlf) 交互
这里我写了一个函数
pyim-cregex-build-lazy。这个函数交互pyim。参见输入法。这个函数的工作逻辑如下:- 如果还没有初始化拼音数据,就先进行预热预热时会加载 "a"、"e"、"o" 这三个字符的拼音映射数据设置初始化标志,避免重复初始化
- 接着分两种情况处理:
- 情况1 :: 单个字符且不是 a/e/o,双个字母不是 zh/ch/sh
- 使用 regexp-quote 直接转义字符
- 例如:输入 "b" → 直接匹配字符 "b"
- 避免触发拼音转换,提高性能
- 情况2 :: 其他所有情况
- 使用 pyim-cregexp-build 进行拼音转换,包括:
- 单个字符 "a"、"e"、"o"(常用韵母)
- 多个字符组合(如 "zh"、"zhong")
- 使用 pyim-cregexp-build 进行拼音转换,包括:
- 情况1 :: 单个字符且不是 a/e/o,双个字母不是 zh/ch/sh
- 设计目的
- 性能优化 :: 避免输入大多数单个字符时的拼音转换开销
- 保持功能 :: 在需要拼音搜索时正常工作
- 用户体验 :: 减少首次输入时的卡顿感
(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))))) - 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.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 。
- 依赖
(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' - 首先尝试导入
- 配置
- 后续的设置宏,用于配置 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") ""))))注意 :语言服务器不会自动安装,如果在新电脑中出现缺少个别语言服务器的情况,我们可以手动安装。
- 弹窗补全优先设置
(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))
- 后续的设置宏,用于配置 Nix 环境下的 LSP 服务器
5.8.3. TODO Treesitter
用于解析语法和语法高亮
- 依赖
# treesit # 目前 treesit 已经内置 treesit-auto
- 配置
(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 用于快速插入模板
- 依赖
这里
yasnippet本体已经作为lsp-bridge的依赖被加入,我们这里只加入一个 snippet 的合集yasnippert-snippets。# yasnippet yasnippet-snippets consult-yasnippet
- 配置
;; (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.6. 自动化环境管理 (direnv)
- 依赖
direnv
- 配置
启用 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.9. 编程与文档语言
5.9.1. Nix
- 依赖
nix-mode
加入 nix 的 major mode。
(idiig//setup-nix-lsp-bridge-server "nix" "nixd" "${pkgs.nixd}/bin" nil)设置 nix 的语言服务器。注意这里使用的是在 LSP-Bridge 节中写的宏(参看 5.8.2)。
- 配置
5.9.3. Python
5.9.4. Haskell
5.9.5. R
5.9.6. Go
- 本体
- 依赖
Major mode:
go-mode
LSP:
(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)
- 依赖
5.9.7. Shell
5.9.8. jq
jq-mode
5.9.9. Make
5.9.10. TeX
5.9.11. Typst
5.9.12. Markdown
5.9.13. Quarto
5.9.14. XML
5.9.15. JSON
5.9.16. Web
5.9.18. PlantUML
5.10. Org相关配置
5.10.1. Org本体
- 绑定 prog mode
(add-hook 'org-mode-hook 'idiig/run-prog-mode-hooks)
- 代码块支持语言
- 依赖
由于
ob-nix,ob-go还没有默认。我们需要添加这些依赖ob-nix ob-go
- 配置
(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"))
- 依赖
- 结构化插入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)) - 基础设定
- 默认进入 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)))) ; 显示所有子节点但不展开 - 允许shift用于选择
(with-eval-after-load 'org (setq org-support-shift-select 2))
- 远程图片文件可以通过
C-u C-c C-x C-v被看到
(with-eval-after-load 'org (setq org-display-remote-inline-images t))
- 允许
#+bind关键词
(with-eval-after-load 'org (setq org-export-allow-bind-keywords t))
- 默认进入 org-mode 时的设定
- 面貌
- 基础美化
(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)) - Bullets
- 依赖
org-bullets
- 配置
(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)
5.10.3. 出入引用配置 (oc, oc-basic)
org-cite 增强:插入后选择样式和参数。
- 配置
(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.11. AI辅助功能
5.11.1. Copilot
- 依赖
copilot
- 配置
(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)
5.11.3. AI-pairing programming (Aidermacs)
- 依赖
- 配置
Aidermacs是一个用于在 Emacs 中使用Aider的 AI 辅助编程工具,集成了多种 AI 模型服务,帮助用户在编程过程中获得智能建议和辅助。- 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))) 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"))
- API Key 环境变量
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>; ... }
]