https://github.com/opsnull/emacs
https://github.com/opsnull/emacs
Last synced: over 1 year ago
JSON representation
- Host: GitHub
- URL: https://github.com/opsnull/emacs
- Owner: opsnull
- Created: 2020-05-25T03:12:55.000Z (about 6 years ago)
- Default Branch: master
- Last Pushed: 2025-01-12T04:55:11.000Z (over 1 year ago)
- Last Synced: 2025-01-28T17:26:48.726Z (over 1 year ago)
- Language: Emacs Lisp
- Homepage:
- Size: 4.59 MB
- Stars: 14
- Watchers: 2
- Forks: 2
- Open Issues: 0
-
Metadata Files:
- Readme: README.org
Awesome Lists containing this project
README
#+Title: My Emacs Dotfile
#+DATE: 2023-08-20 Sun
#+LASTMOD: 2025-01-05T20:01:32+0800
#+AUTHOR: 张俊(geekard@qq.com)
#+STARTUP: overview hideblocks
#+PROPERTY: header-args:emacs-lisp :tangle yes :eval no :results silent :exports code
#+HUGO_BASE_DIR: ~/blog/blog.opsnull.com
#+HUGO_SECTION: emacs
#+HUGO_BUNDLE: my-emacs-dotfile
#+EXPORT_FILE_NAME: index
#+HUGO_AUTO_SET_LASTMOD: t
#+HUGO_TAGS: emacs
#+HUGO_LOCALE: zh
#+OPTIONS: title:t prop:t ^:nil
#+HUGO_WEIGHT: -1
Emacs 是 Hacker 的一种生活方式,而这是符合我口味的私人定制。
#+hugo: more
更多 Emacs 内容请访问[[https://blog.opsnull.com/emacs/][我的博客]]。
#+ATTR_HTML: :width 400 :align center
[[file:static/images/2024-09-03_10-55-30_screenshot.png]]
* 编译安装
从源码编译、安装最新 Emacs 30 版本:
#+begin_src bash :tangle ~/.emacs.d/init.sh
brew uninstall emacs-plus@30
brew install emacs-plus@30 --HEAD --with-xwidgets --with-imagemagick --with-dragon-icon --with-native-comp
brew unlink emacs-plus@30 && brew link emacs-plus@30
ln -sf /opt/homebrew/opt/emacs-plus@30/Emacs.app /Applications/
#+end_src
* 初始化
安装 native 编译所需的 gcc 工具链:
#+begin_src bash :tangle ~/.emacs.d/init.sh
brew install gcc
current_gcc="/opt/homebrew/opt/gcc/lib/gcc/current"
export LIBRARY_PATH="${LIBRARY_PATH}:${current_gcc}:${current_gcc}/gcc/aarch64-apple-darwin23/14/"
#+end_src
=early-init.el= 是 Emacs 启动早期执行的第一个初始化文件:
#+begin_src emacs-lisp :tangle ~/.emacs.d/early-init.el
;; 个人信息。
(setq user-full-name "zhangjun")
(setq user-mail-address "geekard@qq.com")
;; native 编译。
(when (fboundp 'native-compile-async)
(setenv "LIBRARY_PATH"
(concat (getenv "LIBRARY_PATH")
":/opt/homebrew/opt/gcc/lib/gcc/current/"
":/opt/homebrew/opt/gcc/lib/gcc/current/gcc/aarch64-apple-darwin23/14/"))
(setq native-comp-speed 4)
(setq native-comp-async-jobs-number 8)
;;(setq inhibit-automatic-native-compilation t)
(setq native-comp-async-report-warnings-errors 'silent))
;; 解决 cmake 和 Xcode 15.0 兼容性问题,否则后续编译 vterm-module 时报错。
;;; 查看当前 Xcode SDK 路径:xcrun --sdk macosx --show-sdk-path
(setenv "SDKROOT" "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk")
;; 加载较新的 .el 文件。
(setq-default load-prefer-newer t)
(setq-default lexical-binding t)
(setq lexical-binding t)
;; 在独立文件保存 Emacs 自动写入的配置参数,避免污染 ~/.emacs 文件。
(setq custom-file (expand-file-name "~/.emacs.d/custom.el"))
(add-hook 'after-init-hook
(lambda ()
(when (file-exists-p custom-file)
(load custom-file))))
;; 非系统标准路径的二进制目录列表(优先查找靠后目录)
(setq my-bin-path
'("/opt/homebrew/bin"
"/opt/homebrew/opt/findutils/libexec/gnubin"
"/opt/homebrew/opt/openjdk/bin"
"/Users/alizj/go/bin"
;; .cargo/bin 的优先级要高于 rustup/bin,这样优先使用编译安装的命令。
"/opt/homebrew/opt/rustup/bin"
"/Users/alizj/.cargo/bin"))
;; 添加到 PATH 环境变量。
(mapc (lambda (p)
(setenv "PATH" (concat p ":" (getenv "PATH"))))
my-bin-path)
;; 添加到 Emacs 搜索路径:Emacs 查找外部程序时使用 `exec-path` 而非 `PATH` 变量。
(let ((paths my-bin-path))
(dolist (path paths)
(setq exec-path (cons path exec-path))))
#+end_src
设置软件包源:
+ =M-x use-package-report= : 查看包加载时间(按 S 排序);
+ =M-x package-vc-upgrade-all= :更新使用 ~:vc~ 指令配置的,从 ~github~ 安装的包;
#+begin_src emacs-lisp
(require 'package)
(setq package-archives
'(("elpa" . "http://mirrors.tuna.tsinghua.edu.cn/elpa/gnu/")
("elpa-devel" . "http://mirrors.tuna.tsinghua.edu.cn/elpa/gnu-devel/")
("melpa" . "http://mirrors.tuna.tsinghua.edu.cn/elpa/melpa/")
("nongnu" . "http://mirrors.tuna.tsinghua.edu.cn/elpa/nongnu/")
("nongnu-devel" . "http://mirrors.tuna.tsinghua.edu.cn/elpa/nongnu-devel/")))
(package-initialize)
(when (not package-archive-contents)
(package-refresh-contents))
(setq use-package-verbose t)
(setq use-package-always-ensure t)
(setq use-package-always-demand t)
(setq use-package-compute-statistics t)
(setq use-package-vc-prefer-newest t)
;; 允许升级 Emacs 内置的包。
;;(setq package-install-upgrade-built-in t)
#+end_src
设置 GPG 加解密:
#+begin_src emacs-lisp
(setq auth-sources '("~/.authinfo.gpg"))
;;(setq auth-source-debug t)
(use-package epa
:config
(setq-default
;; 缺省使用 email 地址加密。
epa-file-encrypt-to user-mail-address
;; 使用 minibuffer 输入 GPG 密码。
epa-pinentry-mode 'loopback)
(require 'epa-file)
(epa-file-enable))
#+end_src
按键调整:
#+begin_src emacs-lisp
;; command 作为 Meta 键。
(setq mac-command-modifier 'meta)
;; option 作为 Super 键。
(setq mac-option-modifier 'super)
;; fn 作为 Hyper 键。
(setq ns-function-modifier 'hyper)
;; 关闭容易误操作的按键。
;; s- 表示 Super,S- 表示 Shift, H- 表示 Hyper:
(let ((keys '(
"s-w"
"C-z"
""
"s-k"
"s-,"
"s-."
"s--"
"s-+"
"C-"
"C-"
"C-M-"
"C-M-"
;;""
;;""
)))
(dolist (key keys)
(global-unset-key (kbd key))))
#+end_src
提升 IO 性能,参考 [[https://github.com/hlissner/doom-emacs/blob/develop/core/core.el][doom core.el]] :
#+begin_src emacs-lisp
(setq process-adaptive-read-buffering nil)
(setq read-process-output-max (* 1024 1024 4))
(setq inhibit-compacting-font-caches t)
(setq-default message-log-max t)
;; Garbage Collector Magic Hack, 提升 GC 性能。
(use-package gcmh
:init
;;(setq gcmh-verbose t)
(setq gcmh-idle-delay 'auto) ;; 缺省 15s
(setq gcmh-auto-idle-delay-factor 10)
(setq gcmh-high-cons-threshold (* 32 1024 1024))
(gcmh-mode 1)
(gcmh-set-high-threshold))
;;(setq garbage-collection-messages t)
(add-hook 'after-init-hook #'garbage-collect t)
#+end_src
* 代理
~MacOS~ 自带的 ~curl~ 不支持 ~socks5~ 代理, 这里安装支持 ~socks5~ 的 ~GNU curl~ 版本:
#+begin_src bash :tangle ~/.emacs.d/init.sh
brew install curl
export PATH="/opt/homebrew/opt/curl/bin:$PATH"
#+end_src
将 ~GNU curl~ 添加到 ~Emacs~ 的 ~PATH~ 环境变量和 ~exec-path~ 变量中:
#+begin_src emacs-lisp
(setq my-coreutils-path "/opt/homebrew/opt/curl/bin/")
(setenv "PATH" (concat my-coreutils-path ":" (getenv "PATH")))
(setq exec-path (cons my-coreutils-path exec-path))
#+end_src
~Emacs~ 使用 ~url-retrieve~ 访问 URL,这里设置它使用 ~curl~ 后端,这样全局可使用 socks5 代理:
#+begin_src emacs-lisp
;; socks5 代理信息。
(setq my/socks-host "127.0.0.1")
(setq my/socks-port 1080)
(setq my/socks-proxy (format "socks5h://%s:%d" my/socks-host my/socks-port))
;; 不经过 socks 代理的 CIDR 或域名列表, 需要同时满足 socks-noproxy 和 NO_RROXY 值要求:
;; + socks-noproxy: 域名是正则表达式, 如 \\.baidu.com;
;; + NO_PROXY: 域名支持 *.baidu.com 或 baidu.com;
;; 所以这里使用的是同时满足两者的域名后缀形式, 如 .baidu.com;
(setq my/no-proxy
'(
"127.0.0.1/32"
"10.0.0.0/8"
"172.0.0.0/8"
"0.0.0.0/32"
"localhost"
"192.168.0.0/16"
".cn"
".alibaba-inc.com"
".taobao.com"
".antfin-inc.com"
".openai.azure.com"
".baidu.com"
".aliyun-inc.com"
".aliyun-inc.test"
))
(setq my/user-agent
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36")
(use-package mb-url-http
:demand
:vc (:url "https://github.com/dochang/mb-url")
:init
(require 'auth-source)
(let ((credential (auth-source-user-and-password "api.github.com")))
(setq github-user (car credential)
github-password (cadr credential))
(setq github-auth (concat github-user ":" github-password))
(setq mb-url-http-backend 'mb-url-http-curl
mb-url-http-curl-program "/opt/homebrew/opt/curl/bin/curl"
mb-url-http-curl-switches
`("-k"
"-x" ,my/socks-proxy
"--keepalive-time" "60"
"--keepalive"
"--max-time" "300"
;;防止 POST 超过 1024 Bytes 时发送 `Expect: 100-continue` 导致 1s 延迟。
"-H" "Expect: ''"
;;"-u" ,github-auth
"--user-agent" ,my/user-agent
))))
;; 开启 socks5 代理。
(defun proxy-socks-enable ()
(interactive)
(require 'socks)
(setq url-gateway-method 'socks
socks-noproxy my/no-proxy
socks-server `("Default server" ,my/socks-host ,my/socks-port 5))
(let ((no-proxy (mapconcat 'identity my/no-proxy ",")))
(setenv "no_proxy" no-proxy))
(setenv "ALL_PROXY" my/socks-proxy)
(setenv "ALL_PROXY" my/socks-proxy)
(setenv "HTTP_PROXY" nil)
(setenv "HTTPS_PROXY" nil)
(advice-add 'url-http :around 'mb-url-http-around-advice))
;; 关闭 socks5 代理。
(defun proxy-socks-disable ()
(interactive)
(require 'socks)
(setq url-gateway-method 'native socks-noproxy nil)
(setenv "all_proxy" "")
(setenv "ALL_PROXY" ""))
;; 默认启动时开启 socks5 代理。
(proxy-socks-enable)
#+end_src
* 界面
关闭部分 UI 元素:
#+begin_src emacs-lisp
(when (memq window-system '(mac ns x))
(tool-bar-mode -1)
(scroll-bar-mode -1)
(menu-bar-mode -1)
(setq use-file-dialog nil)
(setq use-dialog-box nil))
#+end_src
光标和行号:
#+begin_src emacs-lisp
;; 高亮当前行。
(global-hl-line-mode t)
(setq global-hl-line-sticky-flag t)
;; 显示行号。
(global-display-line-numbers-mode t)
;; 设置光标样式。
(setq-default cursor-type 'bar)
;; 光标和字符宽度一致(如 TAB)。
(setq x-stretch-cursor t)
#+end_src
Frame 设置:
#+begin_src emacs-lisp
;; frame 边角样式:undecorated, round corner: undecorated-round
(add-to-list 'default-frame-alist '(undecorated . t))
(add-to-list 'default-frame-alist '(ns-transparent-titlebar . t))
(add-to-list 'default-frame-alist '(selected-frame) 'name nil)
(add-to-list 'default-frame-alist '(ns-appearance . dark))
;; 新建 frame window 的大小。
(add-to-list 'default-frame-alist '(height . 24))
(add-to-list 'default-frame-alist '(width . 80))
;; 不在新 frame 打开文件(如 Finder 的 "Open with Emacs") 。
(setq ns-pop-up-frames nil)
;; 复用当前 frame。
(setq display-buffer-reuse-frames t)
(setq frame-resize-pixelwise t)
;; 30: 左右分屏, nil: 上下分屏。
(setq split-width-threshold nil)
;; 刷新显示。
(global-set-key (kbd "") #'redraw-display)
(setq switch-to-buffer-obey-display-actions t)
;; 在 frame 底部显示的窗口列表。
(add-to-list
'display-buffer-alist
`((,(regexp-opt
'("\\*compilation\\*"
"\\*Apropos\\*"
"\\*Help\\*"
"\\*helpful"
"\\*info\\*"
"\\*Summary\\*"
"\\*vt"
"\\*lsp-bridge"
"\\*Org"
"\\*Google Translate\\*"
" \\*eglot"
"Shell Command Output"))
;; 复用同名 buffer 窗口。
(display-buffer-reuse-window
. (
;; 在 frame 底部显示窗口。
(side . bottom)
;; 窗口高度比例。
(window-height . 0.35)
)))))
;; 启动后显示模式,加 t 参数让 togg-frame-XX 最后运行,这样才生效:
(add-hook 'window-setup-hook 'toggle-frame-maximized t) ;; toggle-frame-fullscreen
#+end_src
窗口和滚动:
#+begin_src emacs-lisp
;; 切换窗口。
(global-set-key (kbd "s-o") #'other-window)
(setq window-combination-resize t)
;; 像素平滑滚动。
(pixel-scroll-precision-mode t)
(setq fast-but-imprecise-scrolling t)
(setq scroll-conservatively 10
scroll-margin 2
scroll-preserve-screen-position t
mouse-wheel-scroll-amount '(2 ((shift) . hscroll))
mouse-wheel-scroll-amount-horizontal 2)
#+end_src
dashboard:
#+begin_src emacs-lisp
(use-package dashboard
:config
(dashboard-setup-startup-hook)
(setq-local global-hl-line-mode nil)
(setq dashboard-banner-logo-title "Happy Hacking & Writing 🎯")
(setq dashboard-projects-backend #'project-el)
(setq dashboard-center-content t)
(setq dashboard-set-heading-icons t)
(setq dashboard-set-navigator t)
(setq dashboard-set-file-icons t)
(setq dashboard-path-max-length 30)
;; 显示 org-mode agenda。
(add-to-list 'dashboard-items '(agenda) t)
(setq dashboard-items '((recents . 20) (projects . 8) (agenda . 3))))
#+end_src
doom-modeline:
#+begin_src emacs-lisp
;; 使用 Symbols Nerd Fonts Mono 在 modeline 上显示 icons,需要单独下载和安装该字体。
(use-package nerd-icons)
(use-package doom-modeline
:hook (after-init . doom-modeline-mode)
:custom
(doom-modeline-buffer-encoding nil)
(doom-modeline-env-version nil)
(doom-modeline-env-enable-rust nil)
(doom-modeline-env-enable-go nil)
(doom-modeline-buffer-file-name-style 'truncate-nil)
(doom-modeline-vcs-max-length 30)
(doom-modeline-github nil)
(doom-modeline-time-icon nil)
(doom-modeline-check-simple-format t)
:config
(display-battery-mode 0)
(column-number-mode t)
(display-time-mode t)
(setq display-time-24hr-format t)
(setq display-time-default-load-average nil)
(setq display-time-load-average-threshold 20)
(setq display-time-format "%H:%M ") ;; 默认:"%m/%d[%w]%H:%M "
(setq indicate-buffer-boundaries (quote left)))
;; 为 vterm-mode 定义简化的 modeline,避免 vterm buffer 内容过多时更新 modeline 影响性能。
(doom-modeline-def-modeline 'my-vterm-modeline
'(buffer-info) ;; 左侧
'(misc-info minor-modes input-method)) ;; 右侧
(add-to-list 'doom-modeline-mode-alist '(vterm-mode . my-vterm-modeline))
#+end_src
dired-sidebar:使用 dired 显示目录,相比 treemacs/neotree 的优势是速度快和使用 dired 按键:
#+begin_src emacs-lisp
(use-package vscode-icon
:commands (vscode-icon-for-file))
(use-package dired-sidebar
:bind (("s-0" . dired-sidebar-toggle-sidebar))
:commands (dired-sidebar-toggle-sidebar)
:init
(add-hook 'dired-sidebar-mode-hook
(lambda ()
(unless (file-remote-p default-directory)
(auto-revert-mode))))
:config
(push 'toggle-window-split dired-sidebar-toggle-hidden-commands)
(push 'rotate-windows dired-sidebar-toggle-hidden-commands)
(setq dired-sidebar-subtree-line-prefix "-")
(setq dired-sidebar-theme 'vscode) ;;'ascii
(setq dired-sidebar-use-term-integration t)
(setq dired-sidebar-use-one-instance t)
(setq dired-sidebar-use-custom-font t)
(setq dired-sidebar-icon-scale 0.1)
;;(setq dired-sidebar-window-fixed nil) ;; 可以手动调整宽度和高度。
;;(setq dired-sidebar-resize-on-open t)
(setq dired-sidebar-should-follow-file t)
(setq dired-sidebar-follow-file-idle-delay 0.5))
#+end_src
字体:英文 Iosevka/Sarasa 和中文 LxgwWenKai,按照 1:1 缩放,在偶数字号的情况下可以实现中英文等宽等高。
+ 英文:[[https://github.com/protesilaos/iosevka-comfy][Iosevka Comfy]];
+ 中文:霞鹜文楷屏幕阅读版 [[https://github.com/lxgw/LxgwWenKai-Screen/releases][LxgwWenKai-Screen]],对字体做了加粗,便于屏幕阅读;
常用字体命令:
+ 查看 Emacs 支持的字体名称: =(print (font-family-list))=
+ 查看光标处字体: =M-x describe-char=
+ 查看 Emacs 支持的字体名称: =(print (font-family-list))=
#+begin_src emacs-lisp
(use-package fontaine
:config
(setq fontaine-latest-state-file (locate-user-emacs-file "fontaine-latest-state.eld"))
(setq fontaine-presets
'((regular) ;; 使用缺省配置。
(t
:default-family "Iosevka Comfy"
:default-weight regular
:default-height 180 ;; 默认字号, 需要是偶数才能实现中英文等宽等高。
:fixed-pitch-family "Iosevka Comfy"
:fixed-pitch-weight nil
:fixed-pitch-height 1.0
:fixed-pitch-serif-family "Iosevka Comfy"
:fixed-pitch-serif-weight nil
:fixed-pitch-serif-height 1.0
:variable-pitch-family "Iosevka Comfy Duo"
:variable-pitch-weight nil
:variable-pitch-height 1.0
:line-spacing nil)))
(fontaine-mode 1)
(add-hook 'enable-theme-functions #'fontaine-apply-current-preset)
(fontaine-set-preset (or (fontaine-restore-latest-preset) 'regular))
(add-hook 'kill-emacs-hook #'fontaine-store-latest-preset))
;; 设置 emoji/symbol 和中文字体。
(defun my/set-font ()
(when window-system
(setq use-default-font-for-symbols nil)
(set-fontset-font t 'emoji (font-spec :family "Apple Color Emoji")) ;; Noto Color Emoji
(set-fontset-font t 'symbol (font-spec :family "Symbola")) ;; Apple Symbols, Symbola
(let ((font (frame-parameter nil 'font))
(font-spec (font-spec :family "LXGW WenKai Screen")))
(dolist (charset '(kana han hangul cjk-misc bopomofo))
(set-fontset-font font charset font-spec)))))
;; Emacs 启动后或 fontaine preset 切换时设置字体。
(add-hook 'after-init-hook 'my/set-font)
(add-hook 'fontaine-set-preset-hook 'my/set-font)
;; 设置字体缩放比例,设置为 1.172 可以确保 2 倍放大后对应的是 22 号偶数字体,这样表格
;; 可以对齐。16 * 1.172 * 1.172 = 21.97(Emacs 取整为 22)。
(setq text-scale-mode-step 1.172)
;; org-table 只使用中英文严格等宽的 LXGW WenKai Mono Screen 字体, 避免中英文不对齐。
(custom-theme-set-faces 'user '(org-table ((t (:family "LXGW WenKai Mono Screen")))))
#+end_src
ef-themes: Emacs 主题列表:https://emacsthemes.com/popular/index.html
#+begin_src emacs-lisp
(use-package ef-themes
:demand
:config
(mapc #'disable-theme custom-enabled-themes)
(setq ef-themes-variable-pitch-ui t)
(setq ef-themes-mixed-fonts t)
(setq ef-themes-headings
'(
;; level 0 是文档 title,1-8 是文档 header。
(0 . (variable-pitch light 1.9))
(1 . (variable-pitch light 1.8))
(2 . (variable-pitch regular 1.7))
(3 . (variable-pitch regular 1.6))
(4 . (variable-pitch regular 1.5))
(5 . (variable-pitch 1.4))
(6 . (variable-pitch 1.3))
(7 . (variable-pitch 1.2))
(8 . (variable-pitch 1.1))
(t . (variable-pitch 1.1))))
(setq ef-themes-region '(intense no-extend neutral)))
#+end_src
自动切换深浅主题:
#+begin_src emacs-lisp
(defun my/load-theme (appearance)
(interactive)
(pcase appearance
('light (load-theme 'ef-light t))
('dark (load-theme 'ef-elea-dark t))))
(add-hook 'ns-system-appearance-change-functions 'my/load-theme)
(add-hook 'after-init-hook (lambda () (my/load-theme ns-system-appearance)))
#+end_src
tab-bar:
#+begin_src emacs-lisp
(use-package tab-bar
:custom
(tab-bar-close-button-show nil)
(tab-bar-new-button-show nil)
(tab-bar-history-limit 20)
(tab-bar-new-tab-choice "*dashboard*")
(tab-bar-show 1)
;; 使用 super + N 切换 tab。
(tab-bar-select-tab-modifiers "super")
:config
;; 去掉最左侧的 < 和 > 。
(setq tab-bar-format '(tab-bar-format-tabs tab-bar-separator))
;; 开启 tar-bar history mode 后才支持 history-back/forward 命令。
(tab-bar-history-mode t)
(global-set-key (kbd "s-f") 'tab-bar-history-forward)
(global-set-key (kbd "s-b") 'tab-bar-history-back)
(global-set-key (kbd "s-t") 'tab-bar-new-tab)
(keymap-global-set "s-n" 'tab-bar-switch-to-next-tab)
(keymap-global-set "s-p" 'tab-bar-switch-to-prev-tab)
(keymap-global-set "s-w" 'tab-bar-close-tab)
;; 为 tab 添加序号,用于快速切换。
(defvar ct/circle-numbers-alist
'((0 . "⓪")
(1 . "①")
(2 . "②")
(3 . "③")
(4 . "④")
(5 . "⑤")
(6 . "⑥")
(7 . "⑦")
(8 . "⑧")
(9 . "⑨"))
"Alist of integers to strings of circled unicode numbers.")
(setq tab-bar-tab-hints t)
(defun ct/tab-bar-tab-name-format-default (tab i)
(let ((current-p (eq (car tab) 'current-tab))
(tab-num (if (and tab-bar-tab-hints (< i 10))
(alist-get i ct/circle-numbers-alist) "")))
(propertize
(concat tab-num
" "
(alist-get 'name tab)
(or (and tab-bar-close-button-show
(not (eq tab-bar-close-button-show
(if current-p 'non-selected 'selected)))
tab-bar-close-button)
"")
" ")
'face (funcall tab-bar-tab-face-function tab))))
(setq tab-bar-tab-name-format-function #'ct/tab-bar-tab-name-format-default)
(global-set-key (kbd "s-1") 'tab-bar-select-tab)
(global-set-key (kbd "s-2") 'tab-bar-select-tab)
(global-set-key (kbd "s-3") 'tab-bar-select-tab)
(global-set-key (kbd "s-4") 'tab-bar-select-tab)
(global-set-key (kbd "s-5") 'tab-bar-select-tab)
(global-set-key (kbd "s-6") 'tab-bar-select-tab)
(global-set-key (kbd "s-7") 'tab-bar-select-tab)
(global-set-key (kbd "s-8") 'tab-bar-select-tab)
(global-set-key (kbd "s-9") 'tab-bar-select-tab))
#+end_src
* 输入法
使用 ~RIME~ 输入法 + ~iDevel/rime-ice~ 雾凇拼音输入法方案。
安装 ~RIME~ 输入法后端引擎 [[https://github.com/rime/librime/releases][librime]] : ~emacs-rime~ 直接和该引擎打交道,不需要安装前端程序
"鼠须管squirrel"。
#+begin_src bash :tangle ~/.emacs.d/init.sh
wget https://github.com/rime/librime/releases/download/1.11.2/rime-5b09f35-macOS-universal.tar.bz2
tar -xvf rime-5b09f35-macOS-universal.tar.bz2
mv ~/.emacs.d/librime/dist{,.bak}
mv dist ~/.emacs.d/librime
# 如果 MacOS Gatekeeper 阻止第三方软件运行,可以暂时关闭它:
sudo spctl --master-disable
# 后续再开启:sudo spctl --master-enable
#+end_src
下载 [[https://github.com/iDvel/rime-ice.git][iDvel/rime-ice]] 雾凇拼音输入法方案:
#+begin_src bash :tangle ~/.emacs.d/init.sh
mv ~/Library/Rime ~/Library/Rime.bak
git clone https://github.com/iDvel/rime-ice --depth=1
mv rime-ice ~/Library/Rime
# 后续可以 git pull 更新 rime-ice。
cd ~/Library/Rime
# 自定义词频文件
cp custom_phrase.txt opsnull_custom_phrase.txt
# 修改其中的 db_name
sed -i -e 's/custom_phrase.txt/opsnull_custom_phrase/g' opsnull_custom_phrase.txt
#+end_src
rime_ice 拼音方案调整(如模糊音,动态词频,自定义词语文件等):
+ 自定义短语:向自定义短语词典文件 =opsnull_custom_phrase.txt= 添加自定义短语,
custom_prase/db_class 为 stabledb,是只读的,不会动态调频。(可以设置为 tabledb 来
动态调频)。
+ 首次添加该文件后需要执行 =M-x rime-deploy= 和 =M-x rime-sync= 生效。
#+begin_src yaml :tangle ~/Library/Rime/rime_ice.custom.yaml
patch:
switches:
- name: ascii_mode
states: [ 中, A ]
- name: ascii_punct # 中英标点
states: [ ¥, $ ]
# 下面这些开关一般用不到, 故关闭(如候选词中不再显示 emoji).
# - name: traditionalization
# states: [ 简, 繁 ]
# reset: 0
# - name: emoji
# states: [ 💀, 😄 ]
# reset: 1
# - name: full_shape
# states: [ 半角, 全角 ]
# reset: 0
# - name: search_single_char # search.lua 的功能开关,辅码查词时是否单字优先
# abbrev: [词, 单]
# states: [正常, 单字]
# reset: 0
translator/spelling_hints: 0 # 不显示候选词的拼音。
translator/always_show_comments: false #不显示候选者的拼音。
translator/enable_user_dict: true # 根据上屏自动调整词频, 否则根据 *.dict.yaml 中的静态定义的词频率。
custom_phrase/user_dict: "opsnull_custom_phrase" # 自定义短语词典文件,权重最高。
speller/algebra:
# 模糊拼音
# 声母
- derive/^([zcs])h/$1/ # z c s → zh ch sh
- derive/^([zcs])([^h])/$1h$2/ # zh ch sh → z c s
#- derive/^l/n/ # n → l
#- derive/^n/l/ # l → n
# 韵母
- derive/eng$/en/
- derive/en$/eng/
- derive/in/ing/
- derive/ing/in/
# 自动纠错(后者用前者替换)
# ai
- derive/^([wghk])ai$/$1ia/ # wia → wai
# ei
- derive/([wfghkz])ei$/$1ie/ # wie → wei
# ie
- derive/([jqx])ie$/$1ei/ # jei → jie
#+end_src
Rime 输入法全局配置,详细参考 [[https://github.com/iDvel/rime-ice/blob/main/default.yaml][iDvel/rime-ice]]
#+begin_src yaml :tangle ~/Library/Rime/default.custom.yaml
patch:
schema_list:
- schema: rime_ice # 只启用 rime_ice 雾凇拼音输入法方案。
menu/page_size: 9 # 显示 9 个候选词。
# 方案选单切换
switcher/hotkeys:
- F4
- "Control+plus" # 按 C-Shit-+ 调出方案选单。
switcher/fold_options: false # 呼出时不折叠。
switcher/abbreviate_options: false # 折叠时不缩写选项
ascii_composer: # 中英文切换
switch_key: # 关闭左边 Shift 中西文切换,而是使用右侧 Shift(避免频繁误按)。
Shift_L: noop
Shift_R: commit_code
key_binder/bindings:
- { when: has_menu, accept: equal, send: Page_Down } # 下一页
- { when: paging, accept: minus, send: Page_Up } # 上一页
- { when: always, accept: "Control+period", toggle: ascii_mode} # 中英文切换
- { when: always, accept: "Control+comma", toggle: ascii_punct} # 中英文标点切换
#- { when: always, accept: "Control+comma", toggle: full_shape} # 全角/半角切换
# 开启 emacs 绑定惯例,这样可以使用 C-x 来修正拼音。需要将这些按键加到
# rime-translate-keybindings变量里后才会生效。 composing 指的是出现候选词列表的时机。
- { When: composing, accept: Control+p, send: Up }
- { when: composing, accept: Control+n, send: Down }
- { when: composing, accept: Control+b, send: Left }
- { when: composing, accept: Control+f, send: Right }
- { when: composing, accept: Control+a, send: Home }
- { when: composing, accept: Control+e, send: End }
- { when: composing, accept: Control+d, send: Delete }
# 从用户数据库中删除误上屏的词语
- { when: composing, accept: Control+k, send: Shift+Delete }
- { when: composing, accept: Control+h, send: BackSpace }
- { when: composing, accept: Control+g, send: Escape }
- { when: composing, accept: Control+bracketleft, send: Escape }
- { when: composing, accept: Control+y, send: Page_Up }
- { when: composing, accept: Alt+v, send: Page_Up }
- { when: composing, accept: Control+v, send: Page_Down }
# 更多按键名称参考: https://github.com/LEOYoon-Tsaw/Rime_collections/blob/master/Rime_description.md
#+end_src
配置 Emacs:
+ =rime-disable-predicates= 定义了一组断言函数,当任一函数断言成立时,Rime 自动将输入法
切换为英文(inline、ascii-inline、ascii-mode 都指的是英文)。如果同时定义了
rime-inline-predicates 变量,则当这两组函数都至少有一个断言成立时才会切换为英文。
+ =rime-predicate-after-alphabet-char-p= 和 =rime-predicate-in-code-string-p= 条件都会导
致不能正确的中英文混排。
#+begin_src emacs-lisp
(use-package rime
:custom
(rime-user-data-dir "~/Library/Rime/")
(rime-librime-root "~/.emacs.d/librime/dist")
(rime-emacs-module-header-root "/opt/homebrew/opt/emacs-plus@30/include")
:hook
(emacs-startup . (lambda () (setq default-input-method "rime")))
:bind
(
:map rime-active-mode-map
;; 在已经激活 Rime 候选菜单时,强制切换到英文直到按回车。
("M-j" . 'rime-inline-ascii)
:map rime-mode-map
;; 强制切换到中文模式.
("M-j" . 'rime-force-enable)
;; 下面这些快捷键需要发送给 rime 来处理, 需要与 default.custom.yaml 文件中的
;; key_binder/bindings配置相匹配。
("C-." . 'rime-send-keybinding) ;; 中英文切换
("C-+" . 'rime-send-keybinding) ;; 输入法菜单
("C-," . 'rime-send-keybinding) ;; 中英文标点切换
;;("C-," . 'rime-send-keybinding) ;; 全半角切换
)
:config
;; 在 modline 高亮输入法图标, 可用来快速分辨分中英文输入状态。
(setq mode-line-mule-info '((:eval (rime-lighter))))
;; 将如下快捷键发送给 rime,同时需要在 rime 的 key_binder/bindings 的部分配置才会生
;; 效。
(add-to-list 'rime-translate-keybindings "C-h") ;; 删除拼音字符
(add-to-list 'rime-translate-keybindings "C-d")
(add-to-list 'rime-translate-keybindings "C-k") ;; 删除误上屏的词语
(add-to-list 'rime-translate-keybindings "C-a") ;; 跳转到第一个拼音字符
(add-to-list 'rime-translate-keybindings "C-e") ;; 跳转到最后一个拼音字符support
;; shift-l, shift-r, control-l, control-r, 只有当使用系统 RIME 输入法时才有效。
(setq rime-inline-ascii-trigger 'shift-r)
;; 临时英文模式, 该列表中任何一个断言返回 t 时自动切换到英文。如果
;; rime-inline-predicates 不为空,则当其中任意一个断言也返回 t 时才会自动切换到英文
;; (inline 等效于 ascii-mode)。自定义 avy 断言函数。
(defun rime-predicate-avy-p () (bound-and-true-p avy-command))
(setq rime-disable-predicates
'(rime-predicate-ace-window-p
rime-predicate-hydra-p
;;rime-predicate-current-uppercase-letter-p
;; 在上一个字符是英文时才自动切换到英文,适合字符串中中英文混合的情况。
;;rime-predicate-in-code-string-after-ascii-p
;; 代码块内不能输入中文, 但注释和字符串不受影响。
;;rime-predicate-prog-in-code-p
;;rime-predicate-avy-p
))
(setq rime-show-candidate 'posframe)
(setq default-input-method "rime")
(setq rime-posframe-properties
(list :background-color "#333333"
:foreground-color "#dcdccc"
:internal-border-width 2))
;; 部分 mode 关闭 RIME 输入法。
(defadvice switch-to-buffer (after activate-input-method activate)
(if (or (string-match "vterm-mode" (symbol-name major-mode))
(string-match "dired-mode" (symbol-name major-mode))
(string-match "image-mode" (symbol-name major-mode))
(string-match "compilation-mode" (symbol-name major-mode))
(string-match "isearch-mode" (symbol-name major-mode))
(string-match "minibuffer-mode" (symbol-name major-mode)))
(activate-input-method nil)
(activate-input-method "rime"))))
#+end_src
* 补全和预览
本部分使用的三方包说明:
1. vertico:minibuffer 补全;
2. corfu:光标处补全;
3. orderless:提供 flex/regex 过滤风格;
4. consult:实时预览和高级过滤;
5. embark:为 minibuffer/buffer 选中的内容提供快捷操作;
6. marginalia:为候选者提供元数据(如 dired 风格的权限、修改时间等);
vertico 提供 minibuffer 自动补全(corfu 提供光标处的自动补全), 可以使用 orderless 过滤候选者:
+ =C-] (abort-recursive-edit)= 关闭 minibuffer 编辑和补全状态。
+ =M-RET(vertico-exit-input)= 退出输入候选模式,直接使用输入的内容,可以是 file 或 buffer name。
+ =M-} (vertico-next-group)= 选择候选者列表中的下一个分组,如不同的 file 或 project。
+ =TAB (vertico-insert)= 插入当前选中的候选者;
#+begin_src emacs-lisp
(use-package vertico
:config
(setq vertico-count 15)
(vertico-mode 1)
(define-key vertico-map (kbd "") #'vertico-directory-delete-char)
(define-key vertico-map (kbd "RET") #'vertico-directory-enter))
(use-package emacs
:init
;; minibuffer 不显示光标。
(setq minibuffer-prompt-properties '(read-only t cursor-intangible t face minibuffer-prompt))
(add-hook 'minibuffer-setup-hook #'cursor-intangible-mode)
;; M-x 只显示当前 mode 支持的命令。
(setq read-extended-command-predicate #'command-completion-default-include-p)
;; 开启 minibuffer 递归编辑。
(setq enable-recursive-minibuffers t))
#+end_src
corf 在光标处显示候选列表和文档, 可以使用 orderless 来过滤候选者:
#+begin_src emacs-lisp
(use-package corfu
:init
(global-corfu-mode 1)
(corfu-popupinfo-mode 1) ;; 显示候选者文档。
:bind
;; 滚动显示 corfu-popupinfo 内容的快捷键。
(:map corfu-popupinfo-map
("C-M-j" . corfu-popupinfo-scroll-up)
("C-M-k" . corfu-popupinfo-scroll-down))
:custom
(corfu-cycle t) ;; 自动轮转。
(corfu-auto t) ;; 自动补全(不需要按 TAB)。
(corfu-auto-prefix 2) ;; 触发自动补全的前缀长度。
(corfu-auto-delay 0.1) ;; 触发自动补全的延迟, 当满足前缀长度或延迟时, 都会自动补全。
(corfu-separator ?\s) ;; 使用 Orderless 过滤分隔符。
(corfu-preselect 'prompt) ;; Preselect the prompt
(corfu-scroll-margin 5)
(corfu-on-exact-match nil) ;; 默认不选中候选者(即使只有一个)。
(corfu-popupinfo-delay '(0.1 . 0.2)) ;; 候选者帮助文档显示延迟。
(corfu-popupinfo-max-width 80)
(corfu-popupinfo-max-height 50)
(corfu-popupinfo-direction '(force-right)) ;; 强制在右侧显示文档。
:config
(defun corfu-enable-always-in-minibuffer ()
(setq-local corfu-auto nil)
(corfu-mode 1))
(add-hook 'minibuffer-setup-hook #'corfu-enable-always-in-minibuffer 1)
;; corfu 支持 eshell 的 pcomplete 自动补全。
(add-hook 'eshell-mode-hook
(lambda ()
(setq-local corfu-auto nil)
(corfu-mode))))
;; 记录 minibuffer 和 corfu 补全历史,后续显示候选者时按照频率排序。
(use-package savehist
:hook (after-init . savehist-mode)
:config
(setq history-length 100)
(setq savehist-save-minibuffer-history t)
(setq savehist-autosave-interval 300)
(add-to-list 'savehist-additional-variables #'corfu-history)
(add-to-list 'savehist-additional-variables 'mark-ring)
(add-to-list 'savehist-additional-variables 'global-mark-ring)
(add-to-list 'savehist-additional-variables 'extended-command-history))
(use-package emacs
:init
;; 总是在弹出菜单中显示候选者。
(setq completion-cycle-threshold nil)
;; 使用 TAB 来 indentation + completion(completion-at-point 默认是 M-TAB) 。
(setq tab-always-indent 'complete))
#+end_src
orderless 补全风格:使用空格分割一个或多个匹配模式,模式无顺序,是 AND 关系。
orderless 默认使用 =orderless-matching-styles= 变量配置的 =正则和字面量= 匹配方式,通过给各模式指定前
缀或后缀字符, 也可以灵活指定其它匹配模式:
+ ~=~ : =orderless-literal=, 字面量匹配;
+ ~~~ : =orderless-flex=, 模糊匹配,如 abc 实际对应正则 a.*b.*c;
+ ~^~ : =orderless-literal-prefix= ,前缀匹配;
+ ~&~ : =orderless-annotation= ,使用 marginalia 等提供的 meta 信息来过滤;
+ ~,~ : =orderless-initialism=, 首字母缩写,如 ,abc 实际对应正则 \") #'org-download-screenshot)
;; 不添加 #+DOWNLOADED: 注释。
(setq org-download-annotate-function (lambda (link) (previous-line 1) "")))
#+end_src
tex 和 PDF 导出:
#+begin_src emacs-lisp
;; 将安装的 tex 二进制目录添加到 PATH 环境变量和 exec-path 变量中,Emacs 执行 xelatex 命令时使用。
(setq my-tex-path "/Library/TeX/texbin")
(setenv "PATH" (concat my-tex-path ":" (getenv "PATH")))
(setq exec-path (cons my-tex-path exec-path))
;; engrave-faces 比 minted 渲染速度更快。
(use-package engrave-faces
:after ox-latex
:config
(require 'engrave-faces-latex)
(setq org-latex-src-block-backend 'engraved)
;; 代码块左侧添加行号。
(add-to-list 'org-latex-engraved-options '("numbers" . "left"))
;; 代码块主题。
(setq org-latex-engraved-theme 'ef-light))
(defun my/export-pdf (backend)
(progn
;;(setq org-export-with-toc nil)
(setq org-export-headline-levels 2))
)
(add-hook 'org-export-before-processing-functions #'my/export-pdf)
;; ox- 为 org-mode 的导出后端包的惯例前缀。
;;(use-package ox-reveal) ;; reveal.js
(use-package ox-gfm :defer t) ;; github flavor markdown
(require 'ox-latex)
(with-eval-after-load 'ox-latex
;; latex image 的默认宽度, 可以通过 #+ATTR_LATEX :width xx 配置。
(setq org-latex-image-default-width "0.7\\linewidth")
;; 使用 booktabs style 来显示表格,例如支持隔行颜色, 这样 #+ATTR_LATEX: 中不需要添加 :booktabs t。
(setq org-latex-tables-booktabs t)
;; 不保存 LaTeX 日志文件(调试时设置为 nil)。
(setq org-latex-remove-logfiles t)
;; 使用支持中文的 xelatex。
(setq org-latex-pdf-process '("latexmk -xelatex -quiet -shell-escape -f %f"))
(add-to-list 'org-latex-classes
'("ctexart"
"\\documentclass[lang=cn,11pt,a4paper,table]{ctexart}
[NO-DEFAULT-PACKAGES]
[PACKAGES]
[EXTRA]"
("\\section{%s}" . "\\section*{%s}")
("\\subsection{%s}" . "\\subsection*{%s}")
("\\subsubsection{%s}" . "\\subsubsection*{%s}")
("\\paragraph{%s}" . "\\paragraph*{%s}")
("\\subparagraph{%s}" . "\\subparagraph*{%s}"))))
;; org export html 格式时需要 htmlize.el 包来格式化代码。
(use-package htmlize)
#+end_src
自定义导出 PDF 的 LaTeX 样式 mystyle.sty: 对于表格,如果列内容过宽则导出的 PDF 中该列的内容会被截断,
可以为表格设置如下属性,将超宽列 align 设置为 X 来解决:
: #+ATTR_LATEX: :environment tabularx :booktabs t :width \linewidth :align l|l|X
#+begin_src latex :tangle ~/emacs/mystyle.sty
\usepackage{wallpaper} % 显示封面图片或页面图片。
\usepackage{color}
\usepackage{xcolor}
\definecolor{winered}{rgb}{0.5,0,0}
\definecolor{lightgrey}{rgb}{0.9,0.9,0.9}
\definecolor{tableheadcolor}{gray}{0.92}
\definecolor{commentcolor}{RGB}{0,100,0}
\definecolor{frenchplum}{RGB}{190,20,83}
% 提示 title
\usepackage[explicit]{titlesec}
% 每个 chapter 另起一页
\newcommand{\sectionbreak}{\clearpage}
\usepackage{titling}
\setlength{\droptitle}{-6em}
% 超链接和书签
\usepackage[colorlinks]{hyperref}
\hypersetup{
pdfborder={0 0 0},
colorlinks=true,
bookmarksopen=true,
bookmarksnumbered=true, % 书签目录显示编号。
linkcolor={winered},
urlcolor={winered},
filecolor={winered},
citecolor={winered},
linktoc=all}
% 安装 noto-cjk 中文字体: git clone https://github.com/googlefonts/noto-cjk.git
\usepackage{fontspec}
\usepackage[utf8x]{inputenc}
\setmainfont{Noto Serif SC}
\setsansfont{Noto Sans SC}[Scale=MatchLowercase]
\setmonofont{Noto Sans Mono CJK SC}[Scale=MatchLowercase]
\setCJKmainfont[BoldFont=Noto Serif SC]{Noto Serif SC}
\setCJKsansfont{Noto Sans SC}
\setCJKmonofont{Noto Sans Mono CJK SC}
\XeTeXlinebreaklocale "zh"
\XeTeXlinebreakskip = 0pt plus 1pt minus 0.1pt
% 添加 email 命令。
\newcommand\email[1]{\href{mailto:#1}{\nolinkurl{#1}}}
% sidewaytable 依赖 rotfloat
\usepackage {rotfloat}
% tabularx 的特殊 align 参数 X 用来对指定列内容自动换行,否则该列内容有可能被截断,
% 解决办法是:在 org-mode 表格前需要加如下属性:
% #+ATTR_LATEX: :environment tabularx :booktabs t :width \linewidth :align l|X
\usepackage{tabularx}
% 美化表格显示效果
\usepackage{booktabs}
% 表格隔行颜色, {1} 开始行, {lightgrep} 奇数行颜色, {} 偶数行颜色(空表示白色)
\rowcolors{1}{lightgrey}{}
\usepackage{parskip}
\setlength{\parskip}{1em}
\setlength{\parindent}{0pt}
\usepackage{etoolbox}
\usepackage{calc}
\usepackage[scale=0.85]{geometry}
%\setlength{\headsep}{5pt}
\usepackage{amsthm}
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{indentfirst}
\usepackage{multicol}
\usepackage{multirow}
\usepackage{linegoal}
\usepackage{graphicx}
\usepackage{fancyvrb}
\usepackage{abstract}
\usepackage{hologo}
\linespread{1}
\graphicspath{{image/}{figure/}{fig/}{img/}{images/}}
\usepackage[font=small,labelfont={bf}]{caption}
\captionsetup[table]{skip=3pt}
\captionsetup[figure]{skip=3pt}
% 下划线、强调和删除线等
\usepackage[normalem]{ulem}
% 列表
\usepackage[shortlabels,inline]{enumitem}
\setlist{nolistsep}
% xeCJK 默认会把黑点用汉字显示,而 Noto 没有这个字体,所以显示效果为一个小点。解决办法是将它设置为
% \bullet, 这样显示为实心黑点。Windows 带的楷体、仿宋没有这个问题。
\setlist[itemize]{label=$\bullet$}
% 或者:
%\renewcommand\labelitemi{\ensuremath{\bullet}}
#+end_src
创建一个 tempel 模板,便于在 org-mode 文件中快速插入导出 PDF 的 tex 配置参数:
+ 如果生成的 pdf 不显示目录,检查文档 ~#+OPTIONS~ 参数中的 ~toc:nil~ 和 ~num: 2~ 是否生效;
#+begin_src emacs-lisp :tangle no
(my-latex "#+DATE: " (format-time-string "%Y-%m-%d %a") n
"#+SUBTITLE: 内部资料,注意保密!
#+AUTHOR: 张俊(zj@opsnull.com)
# 中文语言环境(目录等用中文显示)。
#+LANGUAGE: zh-CN
# 不自动输出 titile 和 toc,后续 latext mystyle 中定制输出。
# 但是需要明确通过 num 控制输出的目录级别。
#+OPTIONS: prop:t title:nil num:2 toc:nil ^:nil
#+LATEX_COMPILER: xelatex
#+LATEX_CLASS: ctexart
#+LATEX_HEADER: \\usepackage{/Users/alizj/emacs/mystyle}
# 定制 PDF 封面和目录。
#+begin_export latex
% 封面页
\\begin{titlepage}
% 插入标题
\\maketitle
% 插入封面图
%\\ThisCenterWallPaper{0.4}{/path/to/image.png}
% 封面页不编号
\\noindent\\fboxsep=0pt
\\setcounter{page}{0}
\\thispagestyle{empty}
\\end{titlepage}
% 摘要页
\\begin{abstract}
这是一个摘要。
\\end{abstract}
% 目录页
\\newpage
\\tableofcontents
\\newpage
#+end_export
")
#+end_src
slide 演示:org-tree-slide 不再活跃维护了,dslide 是它的的替代品。
#+begin_src emacs-lisp
(use-package dslide
:vc(:url "https://github.com/positron-solutions/dslide.git")
:hook
((dslide-start
.
(lambda ()
(org-fold-hide-block-all)
(setq-default x-stretch-cursor -1)
(redraw-display)
(blink-cursor-mode -1)
(setq cursor-type 'bar)
;;(org-display-inline-images)
;;(hl-line-mode -1)
(text-scale-increase 2)
(read-only-mode 1)))
(dslide-stop
.
(lambda ()
(blink-cursor-mode +1)
(setq-default x-stretch-cursor t)
(setq cursor-type t)
(text-scale-increase 0)
;;(hl-line-mode 1)
(read-only-mode -1))))
:config
(setq dslide-margin-content 0.5)
(setq dslide-animation-duration 0.5)
(setq dslide-margin-title-above 0.3)
(setq dslide-margin-title-below 0.3)
(setq dslide-header-email nil)
(setq dslide-header-date nil)
(define-key org-mode-map (kbd "") #'dslide-deck-start)
(define-key dslide-mode-map (kbd "") #'dslide-deck-stop))
#+end_src
journal 日记:
#+begin_src emacs-lisp
(use-package org-journal
:commands org-journal-new-entry
:bind (("C-c j" . org-journal-new-entry))
:init
(setq org-journal-prefix-key "C-c j")
(defun org-journal-save-entry-and-exit()
(interactive)
(save-buffer)
(kill-buffer-and-window))
:config
(define-key org-journal-mode-map (kbd "C-c C-e") #'org-journal-save-entry-and-exit)
(define-key org-journal-mode-map (kbd "C-c C-j") #'org-journal-new-entry)
(global-set-key (kbd "C-c C-j") #'org-journal-new-entry)
;; 设置日志文件头。
(defun org-journal-file-header-func (time)
"Custom function to create journal header."
(concat
(pcase org-journal-file-type
(`daily "#+TITLE: Daily Journal\n#+STARTUP: showeverything")
(`weekly "#+TITLE: Weekly Journal\n#+STARTUP: folded")
(`monthly "#+TITLE: Monthly Journal\n#+STARTUP: folded")
(`yearly "#+TITLE: Yearly Journal\n#+STARTUP: folded"))))
(setq org-journal-file-header 'org-journal-file-header-func)
(setq org-journal-file-type 'daily) ;; 按天记录。
(setq org-journal-dir "~/docs/org/journal")
(setq org-journal-find-file 'find-file)
;; 加密日记文件。
(setq org-journal-enable-encryption t)
(setq org-journal-encrypt-journal t)
(defun my-old-carryover (old_carryover)
(save-excursion
(let ((matcher (cdr (org-make-tags-matcher org-journal-carryover-items))))
(dolist (entry (reverse old_carryover))
(save-restriction
(narrow-to-region (car entry) (cadr entry))
(goto-char (point-min))
(org-scan-tags '(lambda ()
(org-set-tags ":carried:"))
matcher org--matcher-tags-todo-only))))))
(setq org-journal-handle-old-carryover 'my-old-carryover))
#+end_src
创建一个 templ 模板,便于在文件开头添加内容,可避免每次打开时提示选择 GPG key:
#+begin_example :tangle no
;; 插入自己的 GnuPG 加密 key。
(my-gpg "# -*- mode:org; epa-file-encrypt-to: (\"geekard@qq.com\") -*-")
#+end_example
ox-hugo 博客:
#+begin_src emacs-lisp
(use-package ox-hugo
:demand
:config
(setq org-hugo-base-dir (expand-file-name "~/blog/blog.opsnull.com/"))
(setq org-hugo-section "posts")
(setq org-hugo-front-matter-format "yaml")
(setq org-hugo-export-with-section-numbers t)
(setq org-export-backends '(go md gfm html latex man hugo))
(setq org-hugo-auto-set-lastmod t))
#+end_src
* 编程开发
** 缩进
缩进层次可视化:
#+begin_src emacs-lisp
(use-package indent-bars
:vc (:url "https://github.com/jdtsmith/indent-bars")
:config
(require 'indent-bars-ts)
:custom
(indent-bars-treesit-support t)
(indent-bars-treesit-ignore-blank-lines-types '("module"))
(indent-bars-treesit-scope
'((python
function_definition
class_definition
for_statement
if_statement
with_statement
while_statement)))
:hook
((python-base-mode
yaml-ts-mode
json-ts-mode
js-ts-mode) . indent-bars-mode))
#+end_src
缩进风格:c/c++/go-mode/kernel 均使用 tab 缩进:
#+begin_src emacs-lisp
;;(setq indent-tabs-mode t)
(setq c-ts-mode-indent-offset 8)
(setq c-ts-common-indent-offset 8)
(setq c-basic-offset 8)
;; kernel 风格:table 和 offset 都是 tab 缩进,而且都是 8 字符。
;; https://www.kernel.org/doc/html/latest/process/coding-style.html
(setq c-default-style "linux")
(setq tab-width 8)
#+end_src
** 括号
彩色括号:
#+begin_src emacs-lisp
(use-package rainbow-delimiters
:hook (prog-mode . rainbow-delimiters-mode))
#+end_src
高亮匹配的括号:
#+begin_src emacs-lisp
(use-package paren
:hook (after-init . show-paren-mode)
:init
(setq show-paren-delay 0.1)
(setq show-paren-when-point-inside-paren t
show-paren-when-point-in-periphery t)
(setq show-paren-style 'parenthesis) ;; parenthesis, expression
(set-face-attribute 'show-paren-match nil :weight 'extra-bold))
#+end_src
智能补全括号:
#+begin_src emacs-lisp
(electric-pair-mode 1)
(setq electric-pair-pairs
'(
(?\" . ?\")
(?\{ . ?\})))
(setq electric-pair-preserve-balance t
electric-pair-delete-adjacent-pairs t
electric-pair-skip-self 'electric-pair-default-skip-self
electric-pair-open-newline-between-pairs t)
#+end_src
** project
project 使用 top-down 方式来检查项目路径中是否存在 .project 文件,所以在上层各路径的
目录中不应该存在 .project 文件,否则会导致判断失败。
1. 手动标记项目根目录:在目录下创建 =.project= 文件
2. 查看当前项目的 project root: =(project-current)=
3. 手动添加 project 目录: =M-x project-remember-projects-under=
4. 调试目录的 project root 识别情况: =(my/project-try-local "/path/to/directory")=
#+begin_src emacs-lisp
(use-package project
:custom
(project-switch-commands
'(
(consult-project-buffer "buffer" ?b)
(project-dired "dired" ?d)
(magit-project-status "magit status" ?g)
(project-find-file "find file" ?p)
(consult-ripgrep "rigprep" ?r)
(vterm-toggle-cd "vterm" ?t)))
(project-vc-merge-submodules nil)
:config
;; project-find-file 忽略的目录或文件列表。
(add-to-list 'vc-directory-exclusion-list "vendor") ;; go
(add-to-list 'vc-directory-exclusion-list "node_modules") ;; node
(add-to-list 'vc-directory-exclusion-list "target") ;; rust
)
(defun my/project-try-local (dir)
"Determine if DIR is a non-Git project."
(catch 'ret
(let ((pr-flags '(
;; 顺着目录 top-down 查找第一个匹配的文件。所以中间目录不能有
;; .project 等文件,否则判断 project root 错误。
("go.mod" "Cargo.toml" "pom.xml" "package.json" ".project" )
;; 以下文件容易导致 project root 判断错误, 故不添加。
;; ("Makefile" "README.org" "README.md")
)))
(dolist (current-level pr-flags)
(dolist (f current-level)
(when-let ((root (locate-dominating-file dir f)))
(throw 'ret (cons 'local root))))))))
(setq project-find-functions '(my/project-try-local project-try-vc))
(cl-defmethod project-root ((project (head local)))
(cdr project))
(defun my/project-discover ()
(interactive)
;; 去掉 "~/go/src/k8s.io/*" 目录。
(dolist (search-path
'("~/go/src/github.com/*"
"~/go/src/github.com/*/*"
"~/go/src/gitlab.*/*/*"))
(dolist (file (file-expand-wildcards search-path))
(when (file-directory-p file)
(message "dir %s" file)
;; project-remember-projects-under 列出 file 下的目录, 分别加到
;; project-list-file 中。
(project-remember-projects-under file nil)
(message "added project %s" file)))))
;; 不将 tramp 项目记录到 projects 文件中,防止 emacs-dashboard 启动时检查 project 卡
;; 住。
(defun my/project-remember-advice (fn pr &optional no-write)
(let* ((remote? (file-remote-p (project-root pr)))
(no-write (if remote? t no-write)))
(funcall fn pr no-write)))
(advice-add 'project-remember-project :around 'my/project-remember-advice)
#+end_src
** magit
#+begin_src emacs-lisp
(setq vc-follow-symlinks t)
;; 自动 revert buffer,确保 modeline 上的分支名正确。
(setq auto-revert-check-vc-info t)
(use-package magit
:custom
;; 在当前 window 中显示 magit buffer。
(magit-display-buffer-function #'magit-display-buffer-same-window-except-diff-v1)
(magit-log-arguments '("-n256" "--graph" "--decorate" "--color"))
;; 按照 word 展示 diff。
(magit-diff-refine-hunk t)
(magit-clone-default-directory "~/go/src/")
:config
;; diff org-mode 时展开内容。
(add-hook 'magit-diff-visit-file-hook (lambda() (when (derived-mode-p 'org-mode)(org-fold-show-entry)))))
#+end_src
git-link 为光标位置生成 git 仓库 URL:
#+begin_src emacs-lisp
(use-package git-link
:config
(setq git-link-use-commit t)
;; 重写 gitlab 的 format 字符串以匹配内部系统。
(defun git-link-commit-gitlab (hostname dirname commit)
(format "https://%s/%s/commit/%s" hostname dirname commit))
(defun git-link-gitlab (hostname dirname filename branch commit start end)
(format "https://%s/%s/blob/%s/%s" hostname dirname
(or branch commit)
(concat filename
(when start
(concat "#"
(if end
(format "L%s-%s" start end)
(format "L%s" start))))))))
#+end_src
** treesit
treesit-auto 自动安装 grammer 和自动将 major-mode remap 到对应的 xx-ts-mode 上。具体
参考变 =treesit-auto-recipe-list= :
#+begin_src emacs-lisp
(use-package treesit-auto
:demand t
:config
(setq treesit-auto-install 'prompt)
(global-treesit-auto-mode))
#+end_src
grammer 被安装到 =~/.emacs.d/tree-sitter=, 如
=~/.emacs.d/tree-sitter/libtree-sitter-python.dylib=
+ 执行 =M-x treesit-auto-install-all= 来安装所有的 treesit modules。
+ 如果要重新安装(升级) grammer, 需要先删除 dylib 文件或 tree-sitter 目录, 重启 emacs
后再执行 =M-x treesit-auto-install-all=.
代码折叠:
#+begin_src emacs-lisp
(use-package treesit-fold
:vc (:url "https://github.com/emacs-tree-sitter/treesit-fold")
:config
(global-set-key (kbd "C-c f f") 'treesit-fold-close)
(global-set-key (kbd "C-c f o") 'treesit-fold-open)
(global-set-key (kbd "C-c f O") 'treesit-fold-open-recursively)
(global-set-key (kbd "C-c f F") 'treesit-fold-close-all)
(global-set-key (kbd "C-c f u") 'treesit-fold-open-all)
(global-set-key (kbd "C-c f t") 'treesit-fold-toggle))
#+end_src
** flymake
flymake 为当前 buffer 提供错误检查/诊断功能,它在如下情况检查(诊断)buffer 是否有错
误,错误信息直接显示在 buffer 区域,并发送给 eldoc:
1. 执行 ~M-x flymake-start~;
2. 超过 ~flymake-no-changes-timeout(默认 0.5)~ ;
3. 保存 buffer 时 (除非设置 flymake-start-on-save-buffer 为 nil);
将 flymake-no-changes-timeout 设置为 nil 后,eglot 不会显示 LSP 实时诊断消息,而是当
保存 buffer 后经过 eglot-send-changes-idle-time 时间后才显示诊断消息,这样可以避免显
示编码过程无意义的错误。
#+begin_src emacs-lisp
(use-package flymake
:config
;; 不自动检查 buffer 错误。
(setq flymake-no-changes-timeout nil)
;; 在行尾显示诊断消息(Emacs 30 开始支持), 'short 只显示一条最重要信息,t 显示所有
;; 信息。
(setq flymake-show-diagnostics-at-end-of-line 'short)
;; 如果 buffer 出现错误的诊断消息,执行 flymake-start 重新触发诊断。
(define-key flymake-mode-map (kbd "C-c C-c") #'flymake-start)
;; 显示诊断错误列表
(global-set-key (kbd "C-s-l") #'consult-flymake)
(define-key flymake-mode-map (kbd "C-s-n") #'flymake-goto-next-error)
(define-key flymake-mode-map (kbd "C-s-p") #'flymake-goto-prev-error))
;; 解决 flymake-no-changes-timeout 为 nil 时诊断延迟的问题。
;;; https://github.com/joaotavora/eglot/issues/1296
;; (cl-defmethod eglot-handle-notification :after
;; (_server (_method (eql textDocument/publishDiagnostics)) &key uri
;; &allow-other-keys)
;; (when-let ((buffer (find-buffer-visiting (eglot-uri-to-path uri))))
;; (with-current-buffer buffer
;; (if (and (eq nil flymake-no-changes-timeout)
;; (not (buffer-modified-p)))
;; (flymake-start t)))))
#+end_src
** eglot
elgot 使用 Emacs 内置的 flymake(而非 flycheck)、xref、eldoc、project 等包。
eglot 使用 flymake 来接收和显示 LSP Server 发送的 publishDiagnostics 事件,这是通过向
flymake-diagnostic-functions hook 添加 'eglot-flymake-backend 实现的。
eglot 默认将 flymake 的 backend 清空,只保留 eglot 自身,可以通过配置 ~(add-to-list
'eglot-stay-out-of 'flymake)~ 来关闭 eglot 对 flymake 的清空行为,这样可以使用自定义的
flymake backends,但后续需要添加 hook 来手动启动和配置 eglot-flymake-backend。
#+begin_src emacs-lisp
(use-package eglot
:demand
:after (flymake)
:preface
(defun my/eglot-eldoc ()
;; eglot will change the completion-category-defaults to flex, BAD!
;; https://github.com/minad/corfu/issues/136#issuecomment-eglot
;; 这里将 completion-category-defaults 设置为 nil,然后在 completion-category-overrides
;; 中设置 eglot 使用 orderless 补全风格。
(setq completion-category-defaults nil)
;; 在 eldoc buffer 开始优先显示 flymake 诊断信息。
(setq eldoc-documentation-functions
(cons #'flymake-eldoc-function
(remove #'flymake-eldoc-function eldoc-documentation-functions)))
)
:hook ((eglot-managed-mode . my/eglot-eldoc))
:bind
(:map eglot-mode-map
("C-c C-a" . eglot-code-actions)
("C-c C-f" . eglot-format-buffer)
("C-c C-r" . eglot-rename)
("C-c C-c" . flymake-start)
("C-c C-d" . eldoc))
:config
;; 将 eglot-events-buffer-size 设置为 0 后将关闭显示 *EGLOT event* bufer,不便于调
;; 试问题。也不能设置的太大,否则可能影响性能。
(setq eglot-events-buffer-size (* 1024 1024 1))
;; 将 flymake-no-changes-timeout 设置为 nil 后,eglot 保存 buffer 内容后,经过 idle
;; time 才会向LSP 发送诊断请求。
(setq eglot-send-changes-idle-time 0.1)
;; 当最后一个源码 buffer 关闭时自动关闭 eglot server。
(customize-set-variable 'eglot-autoshutdown t)
(customize-set-variable 'eglot-connect-timeout 60)
;;不给所有 prog-mode 都开启 eglot,否则当它没有 language server 时 eglot 报错。
;;
;;由于 treesit-auto 已经对 major-mode 做了 remap ,需要对 xx-ts-mode-hook 添加 hook,
;;而不是以前的 xx-mode-hook, 否则添加到 xx-mode-hook 的内容不会被自动执行。
(add-hook 'c-ts-mode-hook #'eglot-ensure)
(add-hook 'go-ts-mode-hook #'eglot-ensure)
(add-hook 'bash-ts-mode-hook #'eglot-ensure)
(add-hook 'python-mode-hook #'eglot-ensure)
(add-hook 'python-ts-mode-hook #'eglot-ensure)
(add-hook 'rust-ts-mode-hook #'eglot-ensure)
(add-hook 'rust-mode-hook #'eglot-ensure)
(add-hook 'yaml-mode-hook #'eglot-ensure)
(add-hook 'yaml-ts-mode-hook #'eglot-ensure)
(setq eglot-ignored-server-capabilities
'(
;;:hoverProvider ;; 显示光标位置信息。
;;:documentHighlightProvider ;; 高亮当前 symbol。
;;:inlayHintProvider ;; 显示 inlay hint 提示。
))
;; 加强高亮的 symbol 效果。
;;(set-face-attribute 'eglot-highlight-symbol-face nil :background "#b3d7ff")
;; t: true, false: :json-false(不是 nil)。
;; gopls 配置参数: https://github.com/golang/tools/blob/master/gopls/doc/settings.setq
(setq-default eglot-workspace-configuration
'((:gopls . ((staticcheck . t)
(usePlaceholders . :json-false)
;; gopls 默认设置 GOPROXY=Off, 可能会导致 package 缺失进
;; 而引起补全异常. 开启 allowImplicitNetworkAccess 后将
;; 关闭 GOPROXY=Off.
;;(allowImplicitNetworkAccess . t)
)))))
#+end_src
consult-eglot 提供 ~consult-eglot-symbols~ 函数,方便选择 workspace 中的 symbol:
#+begin_src emacs-lisp
(use-package consult-eglot
:after (eglot consult))
#+end_src
下载 [[https://github.com/blahgeek/emacs-lsp-booster][emacs-lsp-booster]] 可执行程序,然后使用 emacs-lsp-booster 来加速 eglot 的响应性能:
#+begin_src emacs-lisp
(use-package eglot-booster
:vc (:url "https://github.com/jdtsmith/eglot-booster")
:after (eglot)
:config (eglot-booster-mode))
#+end_src
** eldoc
eldoc 在 minibuffer echo-area 或 eldoc buffer 中显示文档和函数签名信息。
#+begin_src emacs-lisp
(use-package eldoc
:after (eglot)
:bind
(:map eglot-mode-map ("C-c C-d" . eldoc))
:config
(setq eldoc-idle-delay 0.1)
;; 打开 eldoc-buffer 时关闭 echo-area 显示, eldoc-buffer 会跟随显示 hover 信息, 如
;; 函数签名。
(setq eldoc-echo-area-prefer-doc-buffer t)
;; 在屏幕右侧显示 eldoc-buffer
(add-to-list 'display-buffer-alist
'("^\\*eldoc.*\\*"
(display-buffer-reuse-window display-buffer-in-side-window)
(dedicated . t)
(side . right)
(inhibit-same-window . t)))
;; 将 minibuffer 窗口高度设为 1,可以确保只显示一行(默认为小数,表示 frame 高度占
;; 比,会导致显示多行)。
(setq max-mini-window-height 1)
;; 为 nil 时只单行显示 eldoc 信息.
(setq eldoc-echo-area-use-multiline-p nil)
;; 一键显示和关闭 eldoc buffer。
(global-set-key (kbd "M-`")
(lambda()
(interactive)
(if (get-buffer-window "*eldoc*")
(delete-window (get-buffer-window "*eldoc*"))
(display-buffer "*eldoc*")))))
#+end_src
注:eglot 不给 eldoc 提供在 echo-area 显示的结构化成员或函数签名信息, 但是可以在 =M-x
eldoc-doc-buffer(C-h-.)= 打开的 eldoc buffer 中会显示这些信息。
eldoc-box 在 frame 右上角或光标位置显示 eldoc-doc-buffer 的内容。
#+begin_src emacs-lisp
(use-package eldoc-box
:after (eglot eldoc)
:bind
(:map eglot-mode-map
("C-M-k" . (lambda () (interactive) (eldoc-box-scroll-down 1)))
("C-M-j" . (lambda () (interactive) (eldoc-box-scroll-up 1)))
;; 按需弹出 posframe 来显示 eldoc buffer 内容。
("C-c C-d" . eldoc-box-help-at-point)
)
:config
(setq eldoc-box-max-pixel-height 600)
(setq eldoc-box-max-pixel-width 1200)
;; C-g 关闭弹出的 child frame。
(setq eldoc-box-clear-with-C-g t)
;; 在右上角显示 eldoc 帮助;
;;(add-hook 'eglot-managed-mode-hook #'eldoc-box-hover-mode t)
;; 在光标位置显示 eldoc 帮助;
;;(add-hook 'eglot-managed-mode-hook #'eldoc-box-hover-at-point-mode t)
)
#+end_src
** python
=brew install python= 目前(2024.03.17)安装的是 python@12 版本,从该版本开始, 如果要 pip
安装 python 包, 必须安装到用户自己的 venv 环境, 否则报错(error:
externally-managed-environment)。具体参考: https://docs.brew.sh/Homebrew-and-Python
#+begin_src shell :tangle no
$ brew reinstall python
$ brew unlink python@3.12 && brew link python@3.12
# 查看安装的位置
$ ls -l $(brew --prefix python)/libexec/bin
$ pip3 install pygments
error: externally-managed-environment
#+end_src
创建一个 =~/.venv= python 虚拟环境, 然后将 pip 包安装到该环境中:
#+begin_src shell :tangle no
zj@a:~$ python3 -m venv .venv
zj@a:~$ source ~/.venv/bin/activate
# 安装相关的包到虚拟环境中
(.venv) zj@a:~$ pip3 install pygments jinji2 ipython markdown flake8 yapf pyright grip debugpy
# 将 /Users/alizj/.venv/bin 添加到 PATH 中,这样后续不需要每次手动 active
# 更新 ~/.bashrc 中的 PATH: PATH=/Users/alizj/.venv/bin:$PATH
#+end_src
配置 Emacs 使用内置的 python-ts-mode 和 venv 虚拟环境:
#+begin_src emacs-lisp
;; 将 ~/.venv/bin 添加到 PATH 环境变量和 exec-path 变量中。
(setq my-venv-path "/Users/alizj/.venv/bin")
(setenv "PATH" (concat my-venv-path ":" (getenv "PATH")))
(setq exec-path (cons my-venv-path exec-path))
;; 指定 python.el 使用虚拟环境目录。
(setq python-shell-virtualenv-root "/Users/alizj/.venv")
(defun my/python-setup-shell (&rest args)
(if (executable-find "ipython3")
(progn
;; 使用 ipython3 作为 python shell.
(setq python-shell-interpreter "ipython3")
(setq python-shell-interpreter-args "--simple-prompt -i --InteractiveShell.display_page=True"))
(progn
;; 查找 python-shell-virtualenv-root 中的解释器.
(setq python-shell-interpreter "python3")
(setq python-interpreter "python3")
(setq python-shell-interpreter-args "-i"))))
;; 使用内置 python mode 和 LSP 来格式化代码(不适用 yapfify)
(use-package python
:init
;;(setq python-indent-guess-indent-offset t)
;;(setq python-indent-guess-indent-offset-verbose nil)
;;(setq python-indent-offset 2)
:hook
(python-mode . (lambda ()
(my/python-setup-shell))))
#+end_src
Python LSP Server 使用 basedpyright,它是微软 VSCode 使用的 pyright 和 pyglance 的开
源 fork 版本。
安装 basepyright:
#+begin_src shell :tangle no
which basedpyright || pip install basedpyright
#+end_src
配置 eglot 使用 basedpyright:
#+begin_src emacs-lisp
(add-to-list 'eglot-server-programs
'((python-mode python-ts-mode)
"basedpyright-langserver" "--stdio"))
#+end_src
pyright _不使用_ pyenv ~.python-version~ 指定的 python 版本或 venv 来搜索依赖的 module,
而是使用=pyrightconfig.json= 文件中配置的 venv 和 venvPath:
+ venvPath:指定查找 venv 目录的上级目录,可以包含多个 venv 环境;
+ venv:指定 venvPath 目录下的、使用的虚拟环境名称, pyright 在该 venv 中搜索依赖的
package;
安装 =pyenv-pyright= 插件来方便的创建和更新 =pyrightconfig.json= 文件:
#+begin_src bash :tangle ~/.emacs.d/init.sh
git clone https://github.com/alefpereira/pyenv-pyright.git $(pyenv root)/plugins/pyenv-pyright
#+end_src
使用方法:
1. 使用 =pyenv local= 为项目指定 ~pyenv virtualenv~;
2. 使用 =pyenv pyright= 来自动配置 =pyrightconfig.json= 使用上一步指定的 virtualenv;
pyright 假设源文件位于项目 scr 目录下,但实际可能会在多个其它子目录(甚至嵌套情况)中
放置项目源码,即 =multi-root= 模式(对应于 vscode 中的多 worksapce 目录),这时可能出现
大量 import 错误,可以通过在项目根目录配置 =pyrightconfig.json= 文件来解决,例如(参考:
python module [[https://github.com/microsoft/pyright/blob/main/docs/import-resolution.md][Import Resolution]]):
#+begin_src javascript :tangle no
{
"venv": "venv-2.7.18",
"venvPath": "/Users/zhangjun/.pyenv/versions",
"verboseOutput": true,
"reportMissingTypeStubs": false,
"executionEnvironments": [
{
"root": "scripts",
"extraPaths": [
".", // scripts 目录下 py 文件导入同级 py 文件的情况
"scripts/appinstance_apply"
]
}
]
}
#+end_src
executionEnvironments:
1. 列表中 root 指定各 workspace 的子目录,是有搜索优先级的,所以如果有相同路径前缀的
情况,应该从长到短依列出来:根据 python 文件的 from/import 语句来确定root 路径:即
从项目根目录(pyrightconfig.json 文件所在目录)开始到文件中导入路径最开始所在目录
之间的目录,都应该是 root。
2. extraPaths 列表中的路径可以是绝对路径或相对路径(相对于 pyrightconfig.json 文件),
用于添加额外的 python module 搜索路径;
+ 添加 "." 是因为需要将 scripts 所在的目录也添加到 module 搜索路径,而不仅仅是 scripts 下的子目录;
3. 官方的实例参考:[[https://github.com/microsoft/pyright/blob/main/docs/configuration.md#sample-config-file][Sample Config File]] 和 [[https://github.com/microsoft/pyright/blob/main/packages/pyright-internal/src/tests/testState.test.ts][testState.test.ts]];
[[https://github.com/Microsoft/pyright/issues/21][pyright 不支持 python 2.x]],如果在上面文件配置 ="pythonVersion": "2.7"= 则会报错。
** go
安装最新 gopls 工具:
#+begin_src bash :tangle ~/.emacs.d/init.sh
go install golang.org/x/tools/gopls@latest
#+end_src
使用 Emacs 内置的 go-ts-mode, 故不需要再单独安装 go-mode 包:
#+begin_src emacs-lisp
(require 'go-ts-mode)
;; go 使用 TAB 缩进。
(add-hook 'go-ts-mode-hook (lambda () (setq indent-tabs-mode t)))
#+end_src
设置 go 环境变量, eglot 启动 gopls 时传递它们:
#+begin_src emacs-lisp
(dolist (env '(("GOPATH" "/Users/alizj/go")
("GOPROXY" "https://goproxy.cn,https://goproxy.io,direct")
("GOPRIVATE" "*.alibaba-inc.com")
("GOOS" "linux")
("GOARCH" "arm64")))
(setenv (car env) (cadr env)))
#+end_src
查看本地和在线 go 文档:
#+begin_src emacs-lisp
(require 'go-ts-mode)
;; 查看光标处符号的本地文档.
(define-key go-ts-mode-map (kbd "C-c d .") #'godoc-at-point)
;; 查看 go std 文档。
(defun my/browser-gostd ()
(interactive)
(xwidget-webkit-browse-url "https://pkg.go.dev/std"))
(define-key go-ts-mode-map (kbd "C-c d s") 'my/browser-gostd)
;; 搜索 pkg.go.dev 在线 web 文档。
(defun my/browser-pkggo (query)
(interactive "ssearch: ")
(xwidget-webkit-browse-url
(concat "https://pkg.go.dev/search?q=" (string-replace " " "%20" query)) t))
(define-key go-ts-mode-map (kbd "C-c d w") 'my/browser-pkggo) ;; 助记: w -> web
#+end_src
安装或更新工具:
#+begin_src emacs-lisp
;; (setq gofmt-command "golangci-lint")
;; (setq gofmt-args "run --config /Users/alizj/.golangci.yml --fix")
(defvar go--tools '("golang.org/x/tools/gopls"
"github.com/rogpeppe/godef"
"golang.org/x/tools/cmd/goimports"
"honnef.co/go/tools/cmd/staticcheck"
"github.com/go-delve/delve/cmd/dlv"
"github.com/zmb3/gogetdoc"
"github.com/josharian/impl"
"github.com/cweill/gotests/..."
"github.com/fatih/gomodifytags"
"github.com/golangci/golangci-lint/cmd/golangci-lint"
"github.com/davidrjenni/reftools/cmd/fillstruct"))
(defun go-update-tools ()
(interactive)
(unless (executable-find "go")
(user-error "Unable to find `go' in `exec-path'!"))
(message "Installing go tools...")
(dolist (pkg go--tools)
(set-process-sentinel
(start-process "go-tools" "*Go Tools*" "go" "install" "-v" "-x" (concat pkg "@latest"))
(lambda (proc _)))))
(use-package go-fill-struct)
(use-package go-impl)
;; 自动为 struct field 添加 json tag。
(use-package go-tag
:init
(setq go-tag-args (list "-transform" "camelcase"))
:config
(require 'go-ts-mode)
(define-key go-ts-mode-map (kbd "C-c t a") #'go-tag-add)
(define-key go-ts-mode-map (kbd "C-c t r") #'go-tag-remove))
(use-package go-playground
:commands (go-playground-mode)
:config
(setq go-playground-init-command "go mod init"))
#+end_src
调试:
1. 如果一个 git 项目下有多个 go module, 则需要在上层目录创建 workspace, 并将各 module
加入其中,否则可能出现 package import 失败的情况:
#+begin_src bash :tangle no
go work init
go work use ./path/to/module1 ./path/to/module2
#+end_src
2. 如果补全或自动提示异常, 执行 ~M-x eglot-events-buffer~ 看是否有报错(例如 GOPROXY=Off
导致的问题.)
** rust
将 Rust 工具链目录添加到 PATH 环境变量和 Emacs 变量 exec-path 中:
+ =~/.cargo/bin= 和 =/opt/homebrew/opt/rustup/bin= 目录已经在初始化时添加到 PATH 和 exec-path 中。
#+begin_src emacs-lisp
;; brew install sccache
(setenv "RUSTC_WRAPPER" "/opt/homebrew/bin/sccache")
#+end_src
配置 rust-mode:
#+begin_src emacs-lisp
;; https://github.com/jwiegley/dot-emacs/blob/master/init.org#rust-mode
(use-package rust-mode
:after (eglot)
:init
(require 'rust-ts-mode)
;; rust-mode 作为 rust-ts-mode 而非 prog-mode 的子 mode。
(setq rust-mode-treesitter-derive t)
:config
;; rust-analyzer 使用 rustfmt 来格式化代码
;;(setq rust-format-on-save t)
(setq rust-rustfmt-switches '("--edition" "2021"))
;; treesit-auto 默认不将 XX-mode-hook 添加到对应的 XX-ts-mode-hook 上, 需要手动指定。
(setq rust-ts-mode-hook rust-mode-hook)
;; rust 建议使用空格而非 TAB 来缩进。
(add-hook 'rust-ts-mode-hook (lambda () (setq indent-tabs-mode nil)))
;; 参数列表参考:https://rust-analyzer.github.io/manual.html#configuration
(add-to-list
'eglot-server-programs
'((rust-ts-mode rust-mode) .
("rust-analyzer"
:initializationOptions
(
:rustfmt
(
:extraArgs ["+nightly"]
)
:completion (:fullFunctionSignatures (:enable t))
;; 20240910 不能关闭 checkOnSave,否则 flymake diagnose 可能不生效。
;;:checkOnSave :json-false
:check
(
:command "clippy"
;;https://esp-rs.github.io/book/tooling/visual-studio-code.html#using-rust-analyzer-with-no_std
:allTargets :json-false
;; 不发送 --workspace 给 cargo check, 只检查当前 package.
;; 20240910 可能导致基于 workspace 的 标准库 lsp 不生效,故不能设置。
:workspace :json-false
)
;;:procMacro (:attributes (:enable t) :enable :json-false)
:cargo
(
;;:buildScripts (:enable :json-false)
;;:features "all"
;;:noDefaultFeatures t
:cfgs (:tokio_unstable "")
;;:autoreload :json-false
)
:diagnostics
(
;;:enable :json-false
:disabled ["unresolved-proc-macro" "unresolved-macro-call"]
)
:inlayHints
(
:bindingModeHints (:enable t)
:closureCaptureHints (:enable t)
:closureReturnTypeHints (:enable t)
:lifetimeElisionHints (:enable t)
:expressionAdjustmentHints (:enable t)
)
;; :linkedProjects
;; [
;; "/Users/alizj/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/std/Cargo.toml",
;; "/Users/alizj/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/Cargo.toml",
;; "/Users/alizj/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/proc_macro/Cargo.toml",
;; "/Users/alizj/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/test/Cargo.toml"
;; ]
)))))
#+end_src
创建如下全局 rustfmt 配置文件;
#+begin_src toml :tangle ~/.rustfmt.toml
# 配置选项参考:
# https://rust-lang.github.io/rustfmt/?version=v1.7.1&search=#chain_width
edition = "2021"
# 确保方法链式调用时,每行一个方法调用,这样 inlayhint 会显示上一个方法的返回值类型。
chain_width = 0
# 是否格式化为单行函数。
fn_single_line = true
# 最大行长度,超过后自动折行。
max_width = 80
# 函数各参数单独一行
fn_args_layout = "Vertical"
# 以下是 unstable features,只能在 nightly channel 使用。
wrap_comments = true
normalize_comments = true
format_code_in_doc_comments = true
comment_width = 80
format_strings = true
imports_granularity = "Crate"
enum_discrim_align_threshold = 20
#+end_src
rust-playground 快速测试环境:
1. BUGFIX: https://github.com/grafov/rust-playground/pull/11/files
2. 设置 Cargo.toml 模板文件变量中 edition 值为 2021(默认是 2018);
#+begin_src emacs-lisp
(use-package rust-playground
:config
(setq rust-playground-cargo-toml-template
"[package]
name = \"foo\"
version = \"0.1.0\"
authors = [\"opsnull \"]
edition = \"2021\"
[dependencies]"))
#+end_src
eglot-x 为 Rust 提供了几个好用的命令:
1. =M-x eglot-x-reload-workspace= :在 Cargo.toml 文件发生变化时手动执行而不需要重启 eglot;
2. =M-x eglot-x-expand-macro= :展开宏定义(或者使用 cargo expand 命令来显示宏展开后的定义);
3. =M-x eglot-x-open-external-documentation= :用浏览器查看光标处的 rust 文档;
#+begin_src emacs-lisp
(use-package eglot-x
:after (eglot rust-mode)
:vc (:url "https://github.com/nemethf/eglot-x")
:init
(require 'rust-ts-mode) ;; 绑定 rust-ts-mode-map 需要。
:config
(eglot-x-setup))
#+end_src
查看本地和在线文档:
#+begin_src emacs-lisp
(with-eval-after-load 'rust-ts-mode
;; 使用 xwidget 打开光标处 symbol 的本地 crate 文档(需要先执行 cargo doc 命令来生成本地文档)
;; RA bug 导致查看 macro 文档的链接是错的:https://github.com/rust-lang/rust-analyzer/issues/16724
(define-key rust-ts-mode-map (kbd "C-c d .") #'eglot-x-open-external-documentation)
;; 查看本地 rust std 文档;
(defun my/browser-ruststd ()
(interactive)
(xwidget-webkit-browse-url "file:///Users/alizj/.rustup/toolchains/stable-aarch64-apple-darwin/share/doc/rust/html/std/index.html" t))
(define-key rust-ts-mode-map (kbd "C-c d s") 'my/browser-ruststd)
;; 在线 https:://docs.rs/ 搜索文档.
(defun my/browser-docsrs (query)
(interactive "ssearch: ")
(xwidget-webkit-browse-url
(concat "https://docs.rs/releases/search?query=" (string-replace " " "%20" query)) t))
(define-key rust-ts-mode-map (kbd "C-c d w") 'my/browser-docsrs) ;; 助记: w -> web
;; 在线搜索 crate 包。
(defun my/search-crates.io (query)
(interactive "ssearch: ")
(xwidget-webkit-browse-url
(concat "https://crates.io/search?q=" (string-replace " " "%20" query)) t))
(global-set-key (kbd "C-c d c") 'my/browser-docsrs) ;; 助记: c -> crates.io
)
#+end_src
cargo package 不再维护, 故切换到 cargo-mode package, 它提供了 Cargo.toml 管理命令。
+ C-c a e(cargo-execute-task) :列出所有支持的 cargo task,如 build/test 等,同时可以添加和删除依赖
(需要指定 PREFIX 命令来输入依赖名称)。
#+begin_src emacs-lisp
(use-package cargo-mode
:after (rust-mode)
:custom
;; cargo-mode 缺省为 compilation buffer 使用 comint mode, 设置为 nil 使用 compilation。
(cargo-mode-use-comint nil)
:hook
(rust-ts-mode . cargo-minor-mode)
:config
;; 自动滚动显示 compilation buffer 内容。
(setq compilation-scroll-output t))
#+end_src
其他技巧:
1. 创建一个 struct 对象时, 可以使用 eglot code-action 来自动填充对象成员;
3. Cargo.toml 文件发生变化时, rust-analyzer 不会自动更新处理, 需要重启 eglot 才能自动补全新的 crate。
两个解决办法:
1. 使用 eglot-x 中的 M-x eglot-x-reload-workspace 命令;
2. 或者先将 =所有依赖= 提前添加到 Cargo.toml 文件, 然后再启动 eglot;
** markdown
安装依赖工具:
#+begin_src bash :tangle ~/.emacs.d/init.sh
brew install multimarkdown
pip3 install grip
#+end_src
multimarkdown 将 markdown 转换为 html 进行 preview,可以结合 xwidget webkit 或 grip
进行实时预览:
#+begin_src emacs-lisp
(use-package markdown-mode
:commands (markdown-mode gfm-mode)
:mode
(("README\\.md\\'" . gfm-mode)
("\\.md\\'" . markdown-mode)
("\\.markdown\\'" . markdown-mode))
:init
(when (executable-find "multimarkdown")
(setq markdown-command "multimarkdown"))
(setq markdown-enable-wiki-links t)
(setq markdown-italic-underscore t)
(setq markdown-asymmetric-header t)
(setq markdown-make-gfm-checkboxes-buttons t)
(setq markdown-gfm-uppercase-checkbox t)
(setq markdown-fontify-code-blocks-natively t)
(setq markdown-gfm-additional-languages "Mermaid")
(setq markdown-content-type "application/xhtml+xml")
(setq markdown-css-paths
'("https://cdn.jsdelivr.net/npm/github-markdown-css/github-markdown.min.css"
"https://cdn.jsdelivr.net/gh/highlightjs/cdn-release/build/styles/github.min.css"))
(setq markdown-xhtml-header-content "
body {
box-sizing: border-box;
max-width: 740px;
width: 100%;
margin: 40px auto;
padding: 0 10px;
}
document.addEventListener('DOMContentLoaded', () => {
document.body.classList.add('markdown-body');
document.querySelectorAll('pre code').forEach((code) => {
if (code.className != 'mermaid') {
hljs.highlightBlock(code);
}
});
});
mermaid.initialize({
theme: 'default', // default, forest, dark, neutral
startOnLoad: true
});
"))
#+end_src
使用 grip 来预览 markdown 文件,它调用 github markdown API 来渲染文件,从而确保渲染后
风格和 Github一致。为了避免 API 调用频率限制,可以创建一个空 scop 的 Access Token,然
后将 username 和 token 保存到 =~/.authinfo.gpg= 文件中:
#+begin_src bash :tangle no
machine api.github.com login geekard@qq.com password YOUR_TOKEN
#+end_src
在 Markdown Buffer 中,执行 =M-x grip-mode= 来启用实时预览,然后可以执行如下命令:
+ M-x grip-start-preview
+ M-x grip-stop-preview
+ M-x grip-restart-preview
+ M-x grip-browse-preview
#+begin_src emacs-lisp
(use-package grip-mode
:defer
:after (markdown-mode)
:config
(setq grip-preview-use-webkit nil)
(setq grip-preview-host "127.0.0.1")
;; 保存文件时才更新预览。
(setq grip-update-after-change nil)
;; 从 ~/.authinfo 文件获取认证信息。
(require 'auth-source)
(let ((credential (auth-source-user-and-password "api.github.com")))
(setq grip-github-user (car credential)
grip-github-password (cadr credential)))
(define-key markdown-mode-command-map (kbd "g") #'grip-mode))
#+end_src
为 markdown 文件添加目录:
#+begin_src emacs-lisp
(use-package markdown-toc
:after(markdown-mode)
:config
(define-key markdown-mode-command-map (kbd "r") #'markdown-toc-generate-or-refresh-toc))
#+end_src
** yaml
使用 emacs 内置的 yaml-ts-mode。
安装 yaml 语言服务器:
#+begin_src shell :tangle no
which yaml-language-server || npm install -g yaml-language-server
#+end_src
yaml 格式说明:
1. 不使用 TAB 而使用空格缩进;
2. 对于多行字符串,使用 name: | 格式,后续第一行缩进必须大于 name: 所在行,而且以后续
以第一行缩进为准来删除后续各行前面的空白,所以各行的缩进必须大于等于第一行的缩进,
超过的部分空白得以保留;
** shell
Emacs 使用 =bash-ts-mode= 来编辑 shell 脚本。
安装 bash language server:
#+begin_src bash :tangle ~/.emacs.d/init.sh
npm i -g bash-language-server
#+end_src
bash language server 使用 shellcheck 工具来做语法检查和静态分析:
#+begin_src bash ~/.emacs.d/init.sh
brew install shellcheck
#+end_src
设置脚本缩进规则:
#+begin_src emacs-lisp
(setq sh-basic-offset 4)
(setq sh-indentation 4)
#+end_src
参考:
1. [[https://google.github.io/styleguide/shellguide.html][Google Shell Style Guide]]
** clang
安装 llvm/clang/clang-format 包:
#+begin_src bash :tangle ~/.emacs.d/init.sh
brew install llvm lld clang-format
export LDFLAGS="-L/opt/homebrew/opt/llvm/lib"
export CPPFLAGS="-I/opt/homebrew/o