🖋️

Clojure開発者のためのEmacs設定 2025(with Vim + Claude Code)

に公開

仕事でClojureを使うことになり、4年半ほど使っていたVSCodeからEmacsの利用に切り替えることにしました。Emacsには過去に5年間ほどSpacemacsというディストリビューションでお世話になったので、その思想を折り込んで設定します。

設定には時間も労力もかかりますが、自分だけのエディタができあがりキーボードだけで高速に操作できる体験は何者にも代えがたいです。AI連携のモダンエディタが華やかなりし今、あえてEmacsを選ぶ方の一助や楽しみとなればと思います。

スクリーンショット

Claude Code連携

SKKによるClaude Codeへの日本語入力の摩擦もEmacs側から操作するので問題にならない

Clojure開発

LSP (Language Server Protocol) + CIDERによるREPL開発もできます

注意

この記事は公開しながら加筆・修正していく予定です。また、EmacsやVimの設定は分かりやすい正解というものがなく、「自分のユースケースに適しているなら短い記述でいいか」となっている箇所もあるのでご容赦ください。また、明らかな間違いを発見された場合は、気軽にコメント頂けると幸いです。

前提

  • Mac + emacs-plus@30がインストール済み
  • 細かい前提条件については、各種設定ファイルの先頭にコメントを記述
  • evilというVimキーバインドを使うパッケージを適用しているため、Vimキーバインドに慣れている方向けです

利用方法

  • 以下2つの設定ファイル(early-init.el, init.el)を~/.emacs.d/以下に配置した上で、Emacsを起動
  • 初回はライブラリのフェッチでとても長い時間がかかり、Warningがいくつか出るが問題ないので無視
  • 2回目以降はスッキリ使えるはず
  • Emacs初心者の方はとりあえずコピペ、少し詳しい方は自分なりにアレンジしてみてください
  • コメントの前方に書いている前提条件や、インストール直後のコマンド実行が必要になってきます

設定 (~/.emacs.d/early-init.el)

EmacsのGUIが描画される前、init.elよりも先にロードされる設定

;;; ~/.emacs.d/early-init.el --- EmacsのGUI起動前に実行される設定ファイル -*- lexical-binding: t; -*-

;;; Commentary:

;; === 前提
;; macOS Sequoia 15.6
;; gcc@15 インストール済み (https://formulae.brew.sh/formula/gcc)
;; libgccjit@15 インストール済み (https://formulae.brew.sh/formula/libgccjit)
;; emacs-plus@30 で利用 (https://github.com/d12frosted/homebrew-emacs-plus)

;; === ネイティブコンパイルをMacで動作させるためパスを通す
(setenv "LIBRARY_PATH"
        (string-join
         '("/opt/homebrew/opt/gcc/lib/gcc/15"
           "/opt/homebrew/opt/libgccjit/lib/gcc/15"
           "/opt/homebrew/opt/gcc/lib/gcc/current/gcc/aarch64-apple-darwin24/15")
         ":"))

;; === GCを抑制し、起動を高速化する
(setq gc-cons-threshold most-positive-fixnum
      gc-cons-percentage 0.6)

;; === 起動後に適切なGC設定に戻す
(add-hook 'emacs-startup-hook
          (lambda ()
            (setq gc-cons-percentage 0.3
                  gc-cons-threshold (* 256 1024 1024)     ; 256MB
                  read-process-output-max (* 2 1024 1024) ; 2MB
                  )
            (add-hook 'focus-out-hook #'garbage-collect)
            (run-with-idle-timer 30 t #'garbage-collect)
            ;; 読み込み後、常に画面を最大化する
            (toggle-frame-maximized)
            )
          )

;; === GUIをスッキリさせる
(menu-bar-mode -1)
(tool-bar-mode -1)
(scroll-bar-mode -1)

;; === ネイティブコンパイルの警告を抑制する
(setq native-comp-async-report-warnings-errors 'silent)

;;====================================================================
;; パッケージ管理のセットアップ
;;===================================================================

;; === packageの設定
(require 'package)
(setq package-archives '(("gnu" . "https://elpa.gnu.org/packages/")
                         ("nongnu" . "https://elpa.nongnu.org/nongnu/")
                         ("melpa" . "https://melpa.org/packages/")))
(setq package-archive-priorities '(("melpa" . 3)
                                   ("nongnu" . 2)
                                   ("gnu" . 1)))
(setq package-check-signature nil) ; 本来はnon-nilが望ましい
(package-initialize)
;; NOTE: 安定してきたら、package-quickstartを検討する

;; === use-packageの設定
(unless (package-installed-p 'use-package)
  (package-install 'use-package))
(require 'use-package)

;;; early-init.el ends here

設定 (~/.emacs.d/init.el)

メイン設定ファイル。1500行近くありますが、これでも少ない方だと思います。

;;; ~/.emacs.d/init.el --- Emacsのメイン設定ファイル -*- lexical-binding: t; -*-

;;; Commentary:

;; === 対象者
;; Clojure開発者を想定
;; Vimキーバインドを利用
;; 日本語入力はddskkを利用

;; === 前提
;; macOS Sequoia 15.6
;; emacs-plus@30 で利用 (https://github.com/d12frosted/homebrew-emacs-plus)

;; === 依存関係
;; (1) gcc@15:  `brew install gcc@15`
;; (2) libgccjit@15 `brew install libgccjit@15`
;; (3) ripgrep `brew install ripgrep`
;; (4) 1Password CLI ([任意] SQLモードの項目でDB接続時のパスワード参照として使っています)
;; (5) JDK: `brew install --cask temurin21`
;; (6) Clojure CLI: `brew install clojure/tools/clojure`
;; (7) clojure-lsp: `brew install clojure-lsp/brew/clojure-lsp-native`
;; (8) docker/orbstack (コンテナを使うなら適宜インストール)
;; (9) Tree-sitterをコンパイルできるもの: `xcode-select --install`

;; === 必要フォント
;; Source Han Code JP (https://github.com/adobe-fonts/source-han-code-jp)
;; UDEV Gothic 35NF (https://github.com/yuru7/udev-gothic)
;; JuliaMono (https://github.com/cormullion/juliamono/releases)

;; === SKK
;; SKKを使いこなすと日本語入力が楽しくなります
;; 以下をEmacs以前に動作するようにセットアップしておいてください(結構面倒です)
;; * macSKK
;;   * https://github.com/mtgto/macSKK
;;   * Emacs以外でもSKKを利用したい
;; * yaskkserv2
;;   * https://github.com/wachikun/yaskkserv2
;;   * SKK言語サーバー(自動でGoogle日本語入力連携)
;;   * Rustのcargoが必要
;; * macism
;;   * https://github.com/laishulu/macism
;;   * Emacs以外とのIME切り替えの摩擦を減らすために必要

;; === ~/.zshrc にあらかじめ以下を追記しておく
;; # eコマンドでサっとEmacsでファイルを開く
;; e() {
;;   if emacsclient --eval "t" > /dev/null 2>&1; then
;;     emacsclient -n "$@"
;;   else
;;     emacs "$@" &
;;   fi
;; }
;;
;; # Emacsのeatターミナルのための設定
;; [ -n "$EAT_SHELL_INTEGRATION_DIR" ] && source "$EAT_SHELL_INTEGRATION_DIR/zsh"

;; === 初回のEmacs起動後に必要なコマンド
;; M-x nerd-icons-install-fonts (nerd-iconsのフォントをインストール)
;; M-x eat-compile-terminfo (Macでeatターミナルの操作不具合を防止)
;; M-x copilot-install-server (GitHub Copilotのサーバー)
;; M-x treesit-install-language-grammar (高速な構文解析)
;;   yaml, https://github.com/ikatyang/tree-sitter-yaml, 後はデフォルト
;;   json, https://github.com/tree-sitter/tree-sitter-json, 後はデフォルト

;;; Code:

;;====================================================================
;; ユーティリティ関数
;;===================================================================
;; === 時間計測
(defvar my-time-tmp (current-time))

(defun my-display-time ()
  "Displays the current time and the elapsed time since the last call."
  (let* ((now (current-time))
         (diff (float-time (time-subtract now my-time-tmp))))
    (setq my-time-tmp now)
    (format "%s dt=%.6f"
            (format-time-string "%Y-%m-%d %H:%M:%S.%6N" now) diff)))

(defun my-insert-time ()
  "Insert measurement template at point."
  (interactive)
  (insert "(message \"[%s] %s\" (my-display-time) \"\")"))

;; === init.elを開く
(defun my-open-user-init ()
  "Open the user's init file."
  (interactive)
  (find-file user-init-file))

;; === カーソル下のシンボルが組み込みのパッケージかどうかチェック
(defun my-package-built-in-p (symbol)
  "Check if SYMBOL is a built-in package."
  (interactive (list (or (symbol-at-point)
                         (intern (read-string "Package: ")))))
  (message "[%s] built-in: %s" symbol (when (package-built-in-p symbol) t)))

;;===== TIPS / Rules of use-package ==================================
;; :ensure 組み込みパッケージにはnil, 外部パッケージにはtを指定
;; :vc GitHubやCodebergなどから直接インストールする場合に利用
;; :after 依存関係 (指定したパッケージのロード後に実行)
;; :init パッケージのロード前に実行
;; :custom defcustom変数はこちらで設定 (key value)の形式
;; :hook パッケージに関連するフックをコンスセル形式で設定 (hook . function)
;;   * 末尾が-hookならそのまま、そうでなければ-hookを付けたものが使われる。関数側は#'をつけない
;; :config 通常通りの処理をまとめる意味 (グローバルスコープになる)
;; 空行は入れないように統一する
;;====================================================================

;; === ローディング開始メッセージ
(message "[%s] %s" (my-display-time) "init.el loading...")

;;====================================================================
;; シェル環境変数をDockからの起動でも利用する
;;====================================================================

(use-package exec-path-from-shell
  :ensure t
  :config
  (exec-path-from-shell-initialize))

;;====================================================================
;; Emacs標準機能の設定
;;====================================================================

;; ===== キーバインド
;; === MacのCommandをMetaキーに
(setq mac-command-modifier 'meta)

;; === C-hをBackspaceに、C-;をC-hに割り当て
(define-key key-translation-map (kbd "C-h") (kbd "DEL"))
(define-key key-translation-map (kbd "C-;") (kbd "C-h"))

;; ===== システム・Emacsコア連携
;; === 自分で配置したEmacsのソースコードへの参照を追加
;; 利用しているEmacsバージョンによって適宜ソースコードはダウンロード必要
(use-package find-func
  :ensure nil
  :config
  (setq find-function-C-source-directory
        (concat "~/Documents/OSS/emacs/emacs-" emacs-version "/src")))

;; === デーモン起動 (シェルの`e'コマンドから使う)
(use-package server
  :ensure nil
  :config
  (unless (server-running-p)
    (server-start)))

;; ===== ファイル管理

;; === バックバップファイルを専用ディレクトリに保存
(setq backup-directory-alist '(("." . "~/.emacs.d/backups")))
(unless (file-exists-p "~/.emacs.d/backups")
  (make-directory "~/.emacs.d/backups" t))

;; === オートセーブファイルを専用ディレクトリに保存
(setq auto-save-file-name-transforms '((".*" "~/.emacs.d/auto-saves/" t)))
(unless (file-exists-p "~/.emacs.d/auto-saves")
  (make-directory "~/.emacs.d/auto-saves" t))

;; === ダイアログでのファイルオープンは使わない
(setq use-file-dialog nil)

;; === 削除したファイルをゴミ箱に移動
(setq delete-by-moving-to-trash t)

;; === 他プロセスの編集をバッファに反映
(global-auto-revert-mode t)

;; === シンボリックリンクを常に質問なしで開く
(setq vc-follow-symlinks t)

;; === 長い行を含むファイルの最適化
(global-so-long-mode t)

;; === 文字列を折り返さないのをデフォルトに
(setq-default truncate-lines t)

;; ===== 編集体験の向上
;; === ミニバッファでのyes/noの聞かれ方をy/nにする
(setq use-short-answers t)

;; === インデントの基本をスペースに変更
(setq-default indent-tabs-mode nil
              tab-width 2
              standard-indent 2)

;; === 対応括弧を補完
(electric-pair-mode t)

;; === ファイル履歴を保存
(use-package recentf
  :ensure nil
  :custom
  (recentf-max-saved-items 50)
  (recentf-mode t))

;; === コマンドの履歴を保存
(savehist-mode t)

;; === ウィンドウの状態を保持
(winner-mode t)

;; === 末尾のスペースやタブを可視化
(defun my-turn-on-show-trailing-ws ()
  "Visualize trailing whitespace buffer-locally."
  (setq-local show-trailing-whitespace t))
(add-hook 'prog-mode-hook #'my-turn-on-show-trailing-ws)
(add-hook 'text-mode-hook #'my-turn-on-show-trailing-ws)

;; === which-keyのディレイ
(use-package which-key
  :ensure nil
  :custom
  (which-key-mode t)
  (which-key-idle-delay 0.3)
  (which-key-idle-secondary-delay 0)
  (which-key-sort-order nil))

;; === *scratch*バッファのデフォルトをtext-modeに
(setq initial-major-mode 'text-mode)
(setq initial-scratch-message "*scratch* for temporal notes\n\n")

;; ==== モード別設定
;; === zshファイルを開いたときにshell-script-modeを有効に
(add-to-list 'auto-mode-alist '("\\zshrc\\'" . shell-script-mode))
(add-to-list 'auto-mode-alist '("\\zprofile\\'" . shell-script-mode))
(add-to-list 'auto-mode-alist '("\\zshenv\\'" . shell-script-mode))

;; === eコマンドで開いたときに別ウィンドウで開く
(setq server-window 'pop-to-buffer)

;;====================================================================
;; バッファの表示方法についての設定 (display-buffer-alist)
;;====================================================================

;; === 後で追加したものが優先される

;; diredを開くときは分割して表示される
(add-to-list 'display-buffer-alist
             '((derived-mode . dired-mode)
               (display-buffer-pop-up-window
                display-buffer-use-some-window)
               (side . right)
               (window-width . 0.5)))

;; dired上でdiredを開くときは同じウィンドウで開く
;; RETでその場で、S-RETで別のウィンドウで開く
(add-to-list 'display-buffer-alist
             '((lambda (buffer-name action)
                 (and (with-current-buffer buffer-name (derived-mode-p 'dired-mode))
                      (with-current-buffer (window-buffer (selected-window))
                        (derived-mode-p 'dired-mode))))
               (display-buffer-same-window)))

;; ielmは分割して開く
(add-to-list 'display-buffer-alist
             '("\\*ielm\\*"
               (display-buffer-pop-up-window
                display-buffer-use-some-window)
               (side . right)
               (window-width . 0.5)))

;; eatは分割して開く
(add-to-list 'display-buffer-alist
             '("\\*eat\\*"
               (display-buffer-pop-up-window
                display-buffer-use-some-window)
               (side . right)
               (window-width . 0.5)
               (window-parameters  . ((dedicated . t)))))

;; flymake-show-project-diagnosticsは右分割で開く
(add-to-list 'display-buffer-alist
             '("\\*Flymake diagnostics"
               (display-buffer-pop-up-window
                display-buffer-use-some-window)
               (side . right)
               (window-width . 0.5)))

;;====================================================================
;; Emacs Lisp用の便利なHelp
;;====================================================================

;; === helpful
(use-package helpful
  :ensure t
  :bind
  (("C-h f" . helpful-callable)
   ("C-h v" . helpful-variable)
   ("C-h k" . helpful-key)
   ("C-h x" . helpful-command)
   ("C-h ." . helpful-at-point)))

;;====================================================================
;; 日本語入力
;;====================================================================

;; === ddskk
;; macism (https://github.com/laishulu/macism) のインストールが必要
(defun my-switch-ime (input-source)
  "Switch to INPUT-SOURCE when Emacs is focused (requires macism command)."
  (call-process "macism" nil 0 nil input-source))

(add-function :after after-focus-change-function
              (lambda ()
                (when (frame-focus-state)
                  (my-switch-ime "net.mtgto.inputmethod.macSKK.ascii"))))

(use-package ddskk
  :ensure t
  :custom
  (skk-server-host "127.0.0.1")
  (skk-server-portnum 1178)
  (skk-dcomp-activate t)
  (skk-egg-like-newline t)
  (skk-delete-implies-kakutei nil)
  (skk-use-color-cursor nil)
  (skk-show-candidates-nth-henkan-char 3)
  (skk-isearch-mode-enable 'always)
  (skk-isearch-mode-string-alist '((hiragana . "")
                                   (katakana . "")
                                   (jisx0208-latin . "")
                                   (latin . "")
                                   (abbrev . "")
                                   (nil . "")))
  :hook
  (isearch-mode-hook . skk-isearch-mode-setup)
  (isearch-mode-hook . skk-latin-mode-on)
  (isearch-mode-end-hook . skk-isearch-mode-cleanup)
  (evil-normal-state-entry-hook . skk-latin-mode-on)
  (text-mode-hook . my-turn-on-skk)
  (prog-mode-hook . my-turn-on-skk)
  :bind
  ("C-x j" . skk-mode)
  ("C-j" . skk-kakutei)
  :config
  (defun my-turn-on-skk ()
    "skk-modeを有効にして、英字モードにする"
    (interactive)
    (skk-mode t)
    (skk-latin-mode-on)))

;;====================================================================
;; UIと外観 (フォントとテーマ)
;;====================================================================

;; === 現在行を強調表示
(global-hl-line-mode t)

;; === 行番号を表示
(add-hook 'prog-mode-hook #'display-line-numbers-mode)
(add-hook 'text-mode-hook #'display-line-numbers-mode)

;; === カーソル位置の列番号をモードラインに表示
(column-number-mode t)

;; === tree-sitterによる色付けmax
(use-package treesit
  :ensure nil
  :custom
  (treesit-font-lock-level 4))

;; === フォント設定
(setq use-default-font-for-symbols nil)
;; Claude Codeの処理中の*マークのアニメーションでガタガタするのを防ぐ
(dolist (char '(#x00B7 #x2722 #x2733 #x2736 #x273B #x273D))
  (set-fontset-font t char (font-spec :family "JuliaMono")))
(set-fontset-font t 'han (font-spec :family "Source Han Code JP") nil 'prepend)
(set-face-attribute 'default nil :font "Source Han Code JP" :height 140)

;; === nerd iconsを利用
;; 初回にM-x nerd-icons-install-fontsの実行が必要(既にインストールされていれば不要)
(use-package nerd-icons
  :ensure t)

;; === カラーテーマ
(use-package catppuccin-theme
  :ensure t
  :custom
  (catppuccin-flavor 'macchiato)
  :config
  (load-theme 'catppuccin t))

;; === カーソルの色をオーバーライド
(set-cursor-color "#cad3f5")

;; === 対応カッコを色付け表示
(use-package rainbow-delimiters
  :ensure t
  :hook
  (prog-mode . rainbow-delimiters-mode))

;; === カラーコードを色付け
(use-package colorful-mode
  :ensure t
  :custom
  (colorful-use-prefix t)
  (global-colorful-mode t))

;; === doom-modeline
(use-package doom-modeline
  :ensure t
  :custom
  (doom-modeline-mode t)
  (doom-modeline-major-mode-icon nil) ; アイコン不要
  (doom-modeline-modal nil) ; evilのモード表示不要
  (doom-modeline-buffer-file-name-style 'file-name) ; ファイル名のみ
  (doom-modeline-percent-position nil) ; 位置の%表示不要
  (doom-modeline-buffer-encoding nil) ; LF/UTF-8などの表示不要
  )

;; === ダッシュボード
(use-package dashboard
  :ensure t
  :custom
  (dashboard-startup-banner 'logo)
  (dashboard-center-content t)
  (dashboard-vertically-center-content t)
  (dashboard-items '((recents . 5)
                     (bookmarks . 5)
                     (projects . 5)))
  :config
  (dashboard-setup-startup-hook))

;; === TODOハイライト
(use-package hl-todo
  :ensure t
  :custom
  (hl-todo-keyword-faces
   `(("TODO" warning bold)
     ("NOTE" ansi-color-cyan bold)
     ("XXX" error bold)))
  (global-hl-todo-mode t))

;; === なめらかなホイールスクロール
(use-package ultra-scroll
  :ensure t
  :vc (:url "https://github.com/jdtsmith/ultra-scroll") ; if desired (emacs>=v30)
  :init
  (setq scroll-conservatively 3
        scroll-margin 0)
  :config
  ;; :customでは適用されない
  (ultra-scroll-mode t))

;;====================================================================
;; EvilによるVimキーバインド
;;====================================================================

(use-package undo-fu
  :ensure t)
(use-package undo-fu-session
  :ensure t
  :after undo-fu
  :custom
  (undo-fu-session-global-mode t))

;; === evilによるVimキーバインドのエミュレート
(use-package evil
  :ensure t
  :after (undo-fu undo-fu-session)
  :custom
  (evil-want-integration t)
  (evil-want-keybinding nil)
  (evil-want-C-u-scroll t)
  (evil-undo-system 'undo-fu)
  (evil-symbol-word-search t) ; ひとかたまりで検索
  (evil-shift-width 2)
  (evil-mode t)
  :bind
  ;; Emacsキーバインドも一部使う
  (:map evil-insert-state-map
        ("C-f" . nil)
        ("C-b" . nil)
        ("C-n" . nil)
        ("C-p" . nil)
        ("C-a" . nil)
        ("C-e" . nil)
        ("C-d" . nil)
        ("C-k" . nil)
        ("C-y" . nil)
        ("C-S-h" . #'backward-kill-sexp))
  (:map evil-motion-state-map
        ("," . nil))
  :config
  ;; 折り返しがある行でgj/gkが面倒なので
  (evil-global-set-key 'motion "j" 'evil-next-visual-line)
  (evil-global-set-key 'motion "k" 'evil-previous-visual-line)
  ;; 最初からnormalモードであってほしいバッファ
  (evil-set-initial-state 'messages-buffer-mode 'normal)
  (evil-set-initial-state 'dashboard-mode 'normal)
  )

;; === evilの便利なキーバインド追加
(use-package evil-collection
  :ensure t
  :after evil
  :custom
  (evil-collection-setup-minibuffer t)
  (evil-collection-setup-debugger-keys t)
  (evil-collection-key-blacklist '("C-j" "C-k" "SPC"))
  (evil-collection-magit-want-horizontal-movement t)
  :config
  (evil-collection-init))

;; === fdでESCできるように
(use-package evil-escape
  :ensure t
  :after evil
  :custom
  (evil-escape-mode t)
  :config
  ;; isearch検索入力中にfキーの反応が遅れるのを防止
  (add-to-list 'evil-escape-inhibit-functions
               (lambda () isearch-mode)))

;; === 囲み系の操作
(use-package evil-surround
  :ensure t
  :after evil
  :custom
  (global-evil-surround-mode t))

;; === 編集操作をハイライト
(use-package evil-goggles
  :ensure t
  :after evil
  :custom
  (evil-goggles-mode t)
  ;; 削除系はハイライトせずとも分かる & 反映が遅れるとストレスなのでオフ
  (evil-goggles-enable-delete nil)
  (evil-goggles-enable-change nil))

;; === 検索ヒット件数を表示
(use-package evil-anzu
  :ensure t
  :after evil
  :custom
  (global-anzu-mode t))

;; === コメントアウト
(use-package evil-commentary
  :ensure t
  :config
  (evil-commentary-mode t))

;; === 数値のインクリメント
(use-package evil-numbers
  :ensure t
  :after evil
  :bind
  (:map evil-normal-state-map
        ("C-a" . evil-numbers/inc-at-pt)))

;;====================================================================
;; ファイルツリー (dired-subtree)
;;====================================================================

;; === dired上でTABでサブディレクトリを展開できる
(use-package dired-subtree
  :ensure t
  :custom
  ;; diredのオプションだがここに書く
  (dired-dwim-target t)
  (insert-directory-program "gls")
  (dired-listing-switches "-alhG --time-style=long-iso"))

;;====================================================================
;; ミニバッファ内での検索・候補選択
;;====================================================================

;; === 便利な統合コマンドの提供 (consult)
(use-package consult
  :ensure t
  :custom
  (consult-async-min-input 2)
  :hook
  (completion-list-mode . consult-preview-at-point-mode))

;; === 補完候補を垂直に表示するUI (vertico)
(use-package vertico
  :ensure t
  :custom
  (vertico-mode t)
  (vertico-cycle t)
  (vertico-count 15)
  (vertico-resize nil))

;; === 柔軟な絞り込みスタイル (orderless)
(use-package orderless
  :ensure t
  :custom
  (completion-styles '(basic partial-completion orderless))
  (completion-category-defaults nil)
  (completion-category-overrides '((file (styles basic))
                                   (corfu (styles basic partial-completion orderless))))
  ;; 正規表現とあいまい検索もデフォルトで有効に
  ;; 完全一致(literal)を優先させたいときは先頭に`='をつける
  (orderless-matching-styles '(orderless-literal
                               orderless-flex
                               orderless-regexp)))

;; === 補完候補に注釈を追加 (marginalia)
(use-package marginalia
  :ensure t
  :after vertico
  :custom
  (marginalia-mode t))

;; === 候補に対するアクション (embark)
(use-package embark
  :ensure t
  :bind
  (("C-." . embark-act)
   ("C-," . embark-export)))

;; === embarkをconsultから使う (embark-consult)
(use-package embark-consult
  :ensure t
  :after (embark consult)
  :hook
  (embark-collect-mode . consult-preview-at-point-mode))

;; === embark-exportしたバッファを直接編集して一括置換などを実現する (wgrep)
(use-package wgrep
  :ensure t
  :custom
  (wgrep-auto-save-buffer t)
  (wgrep-change-readonly-file t))

;;===== Workflow of Replace =========================================
;; 1. `SPC s s' (consult-line)や `SPC s p' (consult-ripgrep)で候補表示
;; 2. `C-,' (embark-export)でembark-collect-modeに
;; 3. OccurやWgrepの違いはあるが, `i'で編集モードに入る
;; 4. `:%s;xxx;yyy;g' などで一括置換 (普通に編集してもいい)
;; 5. `ESC'で編集モードを抜ける (この際に変更を保存するか聞かれることもある)
;;====================================================================

;;====================================================================
;; バッファ内のインライン/ポップアップ補完
;;====================================================================

;; === バッファ内補完のUIフロントエンド (corfu)
(use-package corfu
  :ensure t
  :custom
  (global-corfu-mode t)
  (corfu-popupinfo-mode t)
  (corfu-history-mode t)
  (corfu-auto t)
  (corfu-auto-delay 0)
  (corfu-popupinfo-delay 0)
  (corfu-auto-prefix 1)
  (corfu-cycle t)
  (corfu-preselect 'prompt)
  (corfu-quit-no-match 'separator)
  (tab-always-indent 'complete)
  (corfu-separator ?\s))

;; === 補完ポップアップ内のアイコン
(use-package nerd-icons-corfu
  :ensure t
  :after (corfu nerd-icons)
  :config
  (add-to-list 'corfu-margin-formatters #'nerd-icons-corfu-formatter))

;;====================================================================
;; LSP (eglot)
;;====================================================================

;; === lsp (eglot)
(use-package eglot
  :ensure nil
  :hook
  (clojure-ts-mode . eglot-ensure)
  :custom
  (eglot-events-buffer-config '(:size nil :format full))
  (eglot-autoshutdown t)
  (eglot-connect-timeout 120)
  (eglot-extend-to-xref t)
  (eldoc-echo-area-use-multiline-p nil)
  :config
  ;; === eglotによるLSP起動
  (defun my-eglot-start ()
    "Start eglot for the current buffer if not already started."
    (interactive)
    (eglot-ensure)))

;; === スニペット・テンプレート (tempel)
(use-package tempel
  :ensure t)

(use-package eglot-tempel
  :ensure t
  :init (eglot-tempel-mode t))

;; === 補完ソースの統合・拡張 (cape)
(use-package cape
  :ensure t
  :hook
  (prog-mode . my-prog-capf)
  (text-mode . my-text-capf)
  :config
  (defun my-prog-capf ()
    (unless (local-variable-p 'my-prog-capf-configured)
      (if (bound-and-true-p eglot--managed-mode)
          (setq-local completion-at-point-functions
                      (cons (cape-capf-super #'eglot-completion-at-point
                                             #'tempel-expand
                                             #'cape-file)
                            completion-at-point-functions))
        (setq-local completion-at-point-functions
                    (cons (cape-capf-super #'tempel-complete
                                           #'cape-file)
                          completion-at-point-functions))))
    (setq-local my-prog-capf-configured t))

  (defun my-text-capf ()
    (unless (local-variable-p 'my-text-capf-configured)
      (setq-local completion-at-point-functions
                  (cons (cape-capf-super #'tempel-complete
                                         #'cape-dabbrev
                                         #'cape-file)
                        completion-at-point-functions)))
    (setq-local my-text-capf-configured t))
  )

;;====================================================================
;; ターミナル (eat)
;;====================================================================

;; === ポップアップ管理 (shackle)
(use-package shackle
  :ensure t
  :custom
  (shackle-rules '(("\\*eat\\*" :popup t :align 'right :size 0.5 :select t)))
  (shackle-mode t))

;; === eat
;; インストール直後に `M-x eat-compile-terminfo' を実行する
;; C-c C-l: eat-line-mode, C-c C-j: eat-semi-char-modeで日本語入力などを制御しよう
(use-package eat
  :ensure t
  :demand t
  :vc (:url "http://codeberg.org/akib/emacs-eat" :rev :newest)
  :custom
  (eat-enable-shell-prompt-annotation nil)
  (eat-enable-yank-to-terminal t)
  :bind
  ("C-'" . my-toggle-eat) ; C-'でターミナルのトグル
  :hook
  ;; Claude Codeなどでnbspが強調されて気になるので
  (eat-mode . (lambda () (setq-local nobreak-char-display nil)))
  (eat-line-mode . my-turn-on-skk)
  :config
  (setq process-adaptive-read-buffering nil)
  (defun my-toggle-eat ()
    "Toggle eat terminal."
    (interactive)
    (let ((eat-window (get-buffer-window "*eat*")))
      (if (and eat-window (eq (selected-window) eat-window))
          (quit-window)
        (eat)))))

;;====================================================================
;; Git操作 (magit・diff-hl・vc)
;;====================================================================

;; === magit
(use-package magit
  :ensure t
  :custom
  (magit-diff-refine-hunk 'all)
  :config
  ;; NOTE 差分表示の色合いをカタムするために複雑なことをしているが、しなくてもいい
  ;; magit-diff-visit-fileは別ウィンドウで開く
  (advice-add 'magit-diff-visit-file :around
              (lambda (orig-fun &rest _args)
                (funcall orig-fun t)))

  ;; magit-diffのとき、vc-diffを使う。未追跡ファイルは単に開く
  (defun my-magit-diff-dwim-with-vc-diff (orig-fun &rest args)
    "Advice function to use `vc-diff` in `magit-status-mode`."
    (if-let (file (and (derived-mode-p 'magit-status-mode)
                       (magit-file-at-point)))
        (if (null (vc-state file))
            (progn
              (message "%s is untracked file." file)
              (view-file-other-window file))
          (with-current-buffer (find-file-noselect file)
            (call-interactively #'vc-diff)))
      (apply orig-fun args)))

  (advice-add 'magit-diff-dwim :around #'my-magit-diff-dwim-with-vc-diff))

;; === フリンジに差分を強調表示 (diff-hl)
(use-package diff-hl
  :ensure t
  :custom
  (global-diff-hl-mode t)
  (diff-hl-flydiff-mode t)
  :hook
  (magit-pre-refresh-hook  . diff-hl-magit-pre-refresh)
  (magit-post-refresh-hook . diff-hl-magit-post-refresh))

;;====================================================================
;; ワークスペース (perspective.el)
;;====================================================================

(use-package perspective
  :ensure t
  :init
  (setq persp-suppress-no-prefix-key-warning t)
  (persp-mode)
  :custom
  (persp-state-default-file "~/.cache/emacs/workspace-default")
  (persp-sort 'created)
  (persp-modestring-short t))

;;====================================================================
;; Clojure/ClojureScript/ClojureDart
;;
;; [依存関係]
;; (1) JDK: `brew install --cask temurin21`
;; (2) Clojure CLI: `brew install clojure/tools/clojure`
;; (3) clojure-lsp: `brew install clojure-lsp/brew/clojure-lsp-native`
;; (4) docker/orbstack
;; (5) Tree-sitterをコンパイルできるもの: `xcode-select --install`
;;====================================================================

;; === clojure-ts-mode
;; 従来のclojure-modeではなくclojure-ts-modeで置き換える
;; (.clj,.cljc,.cljs,.cljd,.edn自動認識)
(use-package clojure-ts-mode
  :ensure t
  :custom
  (clojure-ts-toplevel-inside-comment-form t)
  :config
  ;; === clojure-lsp用キャッシュの削除 & 再起動
  (defun my-clojure-lsp-clear-cache-and-restart ()
    "Clear clojure-lsp cache and restart the server."
    (interactive)
    (call-interactively #'eglot-shutdown)
    (let* ((root-dir (project-root (project-current)))
           (lsp-cache (file-name-concat root-dir ".lsp/.cache"))
           (kondo-cache (file-name-concat root-dir ".clj-kondo/.cache")))
      (when (file-directory-p lsp-cache)
        (delete-directory lsp-cache t)
        (message "[%s] Deleted clojure-lsp cache at %s" (my-display-time) lsp-cache))
      (when (file-directory-p kondo-cache)
        (delete-directory kondo-cache t)
        (message "[%s] Deleted clj-kondo cache at %s" (my-display-time) kondo-cache))
      )
    (eglot-ensure)))

(use-package cider
  :ensure t
  :hook (clojure-ts-mode . cider-mode)
  :custom
  (cider-repl-buffer-size-limit 10000)
  (cider-font-lock-dynamically '(macro core function var deprecated))
  (cider-repl-pop-to-buffer-on-connect nil))

;; 構造的編集 (puni)
(use-package puni
  :ensure t
  :demand t
  :hook
  ((emacs-lisp-mode . puni-mode)
   (lisp-interaction-mode . puni-mode)
   (clojure-ts-mode . puni-mode))
  :config
  ;; === puniでカーソル以降をそのレベルで閉じるまで削除
  (defun my-puni-kill-to-end ()
    "Kill to the end of the current sexp."
    (interactive)
    (let ((end (save-excursion (puni-end-of-sexp) (point))))
      (kill-region (point) end)))

  ;; === puniでシンボルを""で囲む
  (defun my-wrap-symbol-with-quotes ()
    "Surround the current symbol with double quotes."
    (interactive)
    (let ((bounds (bounds-of-thing-at-point 'symbol)))
      (when bounds
        (save-excursion
          (goto-char (cdr bounds))
          (insert "\"")
          (goto-char (car bounds))
          (insert "\""))))))

;; Javaライブラリのジャンプ時などに
(use-package jarchive
  :ensure t
  :after eglot
  :config
  (jarchive-setup))

;;====================================================================
;; Markdown
;;====================================================================

(use-package markdown-mode
  :ensure t
  :mode ("\\.md\\'" . gfm-mode)
  :init
  (setq markdown-command '("pandoc" "--from=markdown" "--to=html5"))

  :custom
  (markdown-fontify-code-blocks-natively t)
  (markdown-indent-on-enter 'indent-and-new-item))

;;====================================================================
;; GitHub Copilot連携
;;====================================================================

;; 初回起動後にM-x copilot-install-serverが必要
(use-package copilot
  :ensure t
  :vc (:url "https://github.com/copilot-emacs/copilot.el" :rev :newest :branch "main")
  :hook
  (prog-mode . copilot-mode)
  (copilot-chat-org-prompt-mode . copilot-mode) ; チャット内自体でも有効化
  :bind
  (:map copilot-completion-map
        ("C-<tab>" . copilot-accept-completion))
  (:map prog-mode-map
        ("M-/" . copilot-complete))
  :custom (copilot-max-char 1000000)    ; 最大文字数を増やす
  :config
  (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 '(emacs-lisp-mode  2)))

(use-package copilot-chat
  :ensure t)

;;====================================================================
;; Claude Code IDE
;;====================================================================
(use-package claude-code-ide
  :ensure t
  :vc (:url "https://github.com/manzaltu/claude-code-ide.el" :rev :newest)
  :custom
  (claude-code-ide-terminal-backend 'eat)
  (claude-code-ide-window-width 0.4)
  (claude-code-ide-focus-claude-after-ediff nil)
  :config
  (claude-code-ide-emacs-tools-setup)

  ;; 崩れないように*claude-code[...]*バッファのフォントを変更
  (defun my-set-font-for-claude-buffer ()
    "Set a specific font for Claude Code IDE buffers."
    (when (string-match-p "^\\*claude-code" (buffer-name))
      (buffer-face-set :family "UDEV Gothic 35NF" :height 140)))
  (add-hook 'buffer-list-update-hook #'my-set-font-for-claude-buffer)

  ;; スクラッチバッファのトグル表示
  (defun my-claude-code-ide-scratch ()
    "Toggle Claude Code scratch buffer."
    (interactive)
    (let* ((project-dir (claude-code-ide--get-working-directory))
           (buffer-name (format "*claude-scratch[%s]*"
                                (file-name-nondirectory (directory-file-name project-dir))))
           (scratch-buffer (get-buffer buffer-name))
           (scratch-window (and scratch-buffer (get-buffer-window scratch-buffer))))

      (cond
       ;; ウィンドウが表示されている場合は閉じる
       (scratch-window
        (delete-window scratch-window))

       ;; バッファは存在するが非表示の場合は表示する
       (scratch-buffer
        (my-claude-code-ide-scratch-show scratch-buffer project-dir))

       ;; バッファが存在しない場合は作成して表示
       (t
        (let ((claude-buffer (get-buffer (claude-code-ide--get-buffer-name project-dir))))
          (unless claude-buffer
            (user-error "Claude Code IDEが起動していません"))
          (let ((new-buffer (get-buffer-create buffer-name)))
            (with-current-buffer new-buffer
              (insert "*Claude Code IDE scratch*\n\n")
              (setq-local claude-scratch-project-dir project-dir)
              (setq-local truncate-lines nil))  ; 折り返しを有効化
            (my-claude-code-ide-scratch-show new-buffer project-dir)))))))

  ;; スクラッチバッファを表示する
  (defun my-claude-code-ide-scratch-show (buffer project-dir)
    "Show scratch buffer in dedicated window."
    (let* ((claude-buffer (get-buffer (claude-code-ide--get-buffer-name project-dir)))
           (claude-window (get-buffer-window claude-buffer))
           (left-window (if claude-window
                            (window-left claude-window)
                          (frame-first-window))))
      (when left-window
        (select-window left-window)
        (let ((new-window (split-window-below -12)))
          (set-window-buffer new-window buffer)
          (set-window-dedicated-p new-window t)
          (select-window new-window)
          (goto-char (point-max))))))

  ;; Claude Code IDE とスクラッチバッファの起動・トグル
  (defun my-claude-code-ide-with-scratch ()
    "Toggle Claude Code IDE with scratch buffer."
    (interactive)
    (let* ((project-dir (claude-code-ide--get-working-directory))
           (claude-buffer-name (claude-code-ide--get-buffer-name project-dir))
           (claude-buffer (get-buffer claude-buffer-name))
           (claude-window (and claude-buffer (get-buffer-window claude-buffer)))
           (scratch-buffer-name (format "*claude-scratch[%s]*"
                                        (file-name-nondirectory (directory-file-name project-dir))))
           (scratch-buffer (get-buffer scratch-buffer-name))
           (scratch-window (and scratch-buffer (get-buffer-window scratch-buffer))))

      (cond
       ;; 両方が表示されている場合は両方を隠す
       ((and claude-window scratch-window)
        (claude-code-ide)  ; Claude Codeのトグル
        (delete-window scratch-window))

       ;; Claude Codeのみ表示されている場合は隠す
       (claude-window
        (claude-code-ide))  ; Claude Codeのトグル

       ;; スクラッチバッファのみ表示されている場合は両方表示
       (scratch-window
        (claude-code-ide)  ; Claude Codeを表示
        (my-claude-code-ide-scratch-show scratch-buffer project-dir)
        ;; スクラッチバッファにフォーカスを移す
        (select-window (get-buffer-window scratch-buffer)))

       ;; Claude バッファは存在するがウィンドウがない場合
       (claude-buffer
        (claude-code-ide)  ; Claude Codeを表示
        (when (or scratch-buffer (not claude-buffer))
          (my-claude-code-ide-scratch))
        (let ((scratch-win (get-buffer-window (get-buffer scratch-buffer-name))))
          (when scratch-win
            (select-window scratch-win))))

       ;; 他のケースは両方表示
       (t
        (claude-code-ide)  ; Claude Codeを表示
        ;; スクラッチバッファも表示
        (when (or scratch-buffer (not claude-buffer))
          (my-claude-code-ide-scratch))
        ;; スクラッチバッファにフォーカスを移す
        (let ((scratch-win (get-buffer-window (get-buffer scratch-buffer-name))))
          (when scratch-win
            (select-window scratch-win)))))))

  ;; 選択範囲の送信またはプロンプト入力
  (defun my-claude-code-ide-send-region-or-prompt ()
    "Send Evil selection to Claude Code or open prompt."
    (interactive)
    (if (and (bound-and-true-p evil-mode)
             (or (evil-visual-state-p)
                 (region-active-p)))
        ;; Evil visual mode での選択がある場合
        (let ((text (buffer-substring-no-properties (region-beginning) (region-end))))
          (when (evil-visual-state-p)
            (evil-exit-visual-state))
          (my-claude-code-ide-send-text text))
      ;; 選択していない場合はデフォルトの prompt コマンド
      (claude-code-ide-send-prompt)))

  ;; 数字を送信するコマンド
  (defun my-claude-code-ide-send-number-1 ()
    "Send '1' to Claude Code."
    (interactive)
    (my-claude-code-ide-send-text "1"))

  (defun my-claude-code-ide-send-number-2 ()
    "Send '2' to Claude Code."
    (interactive)
    (my-claude-code-ide-send-text "2"))

  (defun my-claude-code-ide-send-number-3 ()
    "Send '3' to Claude Code."
    (interactive)
    (my-claude-code-ide-send-text "3"))

  ;; テキストを Claude Code に送信
  (defun my-claude-code-ide-send-text (text)
    "Send TEXT to Claude Code."
    (let* ((project-dir (claude-code-ide--get-working-directory))
           (claude-buffer-name (claude-code-ide--get-buffer-name project-dir))
           (claude-buffer (get-buffer claude-buffer-name)))
      (unless claude-buffer
        (user-error "Claude Code IDEが起動していません"))
      (with-current-buffer claude-buffer
        (claude-code-ide--terminal-send-string text)
        (sit-for 0.1)
        (claude-code-ide--terminal-send-return))
      (message "Claude Codeに送信しました: %s" (substring text 0 (min 50 (length text))))))
  )

;;====================================================================
;; Dockerコンテナ内開発ワークフロー
;;====================================================================

(use-package dockerfile-mode
  :ensure t
  :mode ("Dockerfile\\'" . dockerfile-mode))

;; === yaml-ts-mode
;; 初回起動時に`M-x treesit-install-language-grammar`を実行し
;; languageとして`yaml`, 手動でURL: https://github.com/ikatyang/tree-sitter-yamlを指定(後はデフォルト)
(use-package yaml-ts-mode
  :ensure nil
  :mode ("\\.ya?ml\\'" . yaml-ts-mode))

;; === json-ts-mode
;; 初回起動時に`M-x treesit-install-language-grammar`を実行し
;; languageとして`json`, 手動でURL: https://github.com/tree-sitter/tree-sitter-jsonを指定(後はデフォルト)
(use-package json-ts-mode
  :ensure nil
  :mode ("\\.json\\'" . json-ts-mode))

(use-package docker
  :ensure t
  :custom
  (docker-container-columns
   '((:name "Names" :width 30 :template "{{ json .Names }}" :sort nil :format nil)
     (:name "Status" :width 30 :template "{{ json .Status }}" :sort nil :format nil)
     (:name "Ports" :width 30 :template "{{ json .Ports }}" :sort nil :format nil)))
  (docker-container-default-sort-key '("Names")))

(add-to-list 'tramp-remote-path 'tramp-own-remote-path)
(setq tramp-default-method "docker")
(setq tramp-default-remote-shell "/bin/bash")

;; docker compose upなどでコンテナが起動していれば
;; M-x find-fileから/docker:コンテナ名:/path/to/fileで接続すればlspもうまく動作

;;====================================================================
;; DB接続
;;====================================================================

(use-package sql
  :ensure nil
  :custom
  (sql-postgres-login-params nil)
  (setq sql-connection-alist
        '((sample-postgres
           (sql-product 'postgres)
           ;; NOTE: DB設定については適宜変更
           (sql-database (concat
                          "postgresql://"
                          "root"
                          ":" (my-read-1password "sample_dev_db")
                          "@localhost"
                          ":5432"
                          "/sample_dev"
                          )))))
  :config
  (setq sql-mode-hook
        `(lambda ()
           (sql-indent-enable)
           (sql-highlight-postgres-keywords)))

  (defun my-read-1password (name)
    "1Passwordからパスワードを取得する"
    (let ((password (shell-command-to-string
                     (format "op read op://Private/%s/password" name))))
      (string-trim password))))

;;====================================================================
;; Format On Save設定の集約
;;====================================================================

;; === Emacs Lisp
(defun my-format-emacs-lisp ()
  "Format the current buffer as Emacs Lisp code."
  (interactive)
  (save-excursion
    (indent-region (point-min) (point-max)))
  (message "[elisp] formatted."))

;; === Clojure
(defun my-format-clojure ()
  "Format the current buffer as Clojure code using cljfmt."
  (interactive)
  ;; プロジェクトルートでのcljfmtの実行
  ;; バッファを消して再度挿入なのでsave-excursionは使えない
  (when-let* ((cljfmt-path (executable-find "cljfmt"))
              (project-root-path (project-root (project-current))))
    (let ((p-current (point))
          (default-directory project-root-path))
      (call-process-region (point-min) (point-max) cljfmt-path t t nil "fix" "-" "--quiet")
      (goto-char p-current)
      (message "[cljfmt] Formatted."))))

;; === フォーマッタの適用方法をここにまとめる
;; 左: メジャーモード, 右: 優先順位をつけたフォーマット方法のリスト
(defvar my-format-rules
  '((emacs-lisp-mode . (my-format-emacs-lisp))
    (clojure-ts-mode . (:lsp my-format-clojure))
    (clojure-ts-clojurescript-mode . (:lsp my-format-clojure))))

(defun my-format-try (formatter)
  "Try to format using FORMATTER."
  (pcase formatter
    (:lsp
     (when (bound-and-true-p eglot--managed-mode)
       (message "[eglot] Formatting via LSP...")
       (call-interactively #'eglot-format-buffer)
       t))

    ((and (pred fboundp) fn)
     (funcall fn)
     t)

    (_ nil)))

(defun my-format-buffer ()
  "Format the current buffer based on its major mode."
  (interactive)
  (let ((formatters (cdr (assoc major-mode my-format-rules))))
    (if (not (cl-some #'my-format-try formatters))
        (message "No suitable formatter found for %s" major-mode))))

(add-hook 'before-save-hook #'my-format-buffer)
(add-hook 'before-save-hook #'whitespace-cleanup) ;; trailing spacesの削除

;;====================================================================
;; キーバインド (general.el)
;;====================================================================

;; use-packageと:generalの組み合わせで色々できる
(use-package general
  :ensure t
  :after (evil evil-collection)
  :config
  (general-evil-setup)
  (general-auto-unbind-keys)

  ;; === リーダーキー定義 (SPC)
  (general-create-definer my-global-leader-def
    :states '(normal visual)
    :keymaps 'override
    :prefix "SPC")

  ;; === ローカルリーダーキー定義 (,)
  (general-create-definer my-local-leader-def
    :states '(normal)
    :keymaps 'override
    :prefix ",")

  ;; === モーション系 (g)
  (general-create-definer my-motion-leader-def
    :states '(normal visual)
    :keymaps 'override
    :prefix "g")

  ;; === 全メジャーモード共通のキーバインド
  (my-global-leader-def
    ;; (SPC) M-x
    "SPC" '(execute-extended-command :wk "M-x")

    ;; (;) コメント
    ";" '(evil-commentary-line :wk "comment")

    ;; (t) トグル
    "t" '(:ignore t :wk "Toggle")
    "t l" '(toggle-truncate-lines :wk "truncate line")
    "t f" '(flymake-mode :wk "toggle flymake")

    ;; (q) 終了操作
    "q" '(:ignore t :wk "Quit")
    "q q" '(save-buffers-kill-terminal :wk "quit")
    "q r" '(restart-emacs :wk "restart")

    ;; (f) ファイル操作
    "f" '(:ignore t :wk "Files")
    "f f" '(find-file :wk "file find")
    "f r" '(recentf-open :wk "file recent")
    "f p" '(project-find-file :wk "find in project")
    "f s" '(save-buffer :wk "file save")
    "f i" '(my-open-user-init :wk "init.el")
    "f t" '(project-dired :wk "dired")

    ;; (b) バッファ操作/ブックマーク
    "b" '(:ignore t :wk "Buffers/Bookmark")
    "b b" '(consult-buffer :wk "buffer switch")
    "b d" '(kill-current-buffer :wk "buffer kill")
    "b h" '(dashboard-open :wk "dashboard home")
    "b l" '(consult-bookmark :wk "bookmark list")
    "b s" '(bookmark-set :wk "bookmark set")
    "b k" '(bookmark-delete :wk "bookmark delete")

    ;; (s) 検索
    "s" '(:ignore t :wk "Search")
    "s s" '(consult-line :wk "search in buffer")
    "s p" '(consult-ripgrep :wk "search in project")
    "s f" '(consult-flymake :wk "search flymake")

    ;; (p) プロジェクト管理
    "p" '(:ignore t :wk "Project/Package")
    "p p" '(project-switch-project :wk "project switch")
    "p f" '(flymake-show-project-diagnostics :wk "project flymake")

    ;; (w) ワークスペース/ウィンドウ操作
    "w" '(:ignore t :wk "Workspace/Window")
    "w w" '(persp-switch :wk "workspace switch")
    "w r" '(persp-rename :wk "workspace rename")
    "w d" '(persp-kill :wk "workspace kill")
    "w s" '(persp-state-save :wk "workspace save")
    "w l" '(persp-state-load :wk "workspace load")
    "w u" '(winner-undo :wk "window undo")

    ;; (g) Git/ジャンプ
    "g" '(:ignore t :wk "Git/GoTo")
    "g s" '(magit-status-quick :wk "git status")
    "g l" '(magit-log-current :wk "git log")
    "g d" '(vc-diff :wk "git diff")

    ;; (d) 差分/デバッグ/Docker/DB
    "d" '(:ignore t :wk "Diff/Debug/Docker/DB")
    "d d" '(diff-hl-show-hunk :wk "diff")
    "d c" '(docker-containers :wk "docker containers")
    "d b" '(sql-connect :wk "db connect")

    ;; (a) 生成AI系
    "a" '(:ignore t :wk "AI")
    "a p" '(copilot-chat :wk "Copilot chat") ; 補完はM-/でサジェスト
    ;; Claude Code IDE (M-RET: 改行, C-ESC: エスケープ)
    "a m" '(claude-code-ide-menu :wk "Claude menu")
    "a a" '(my-claude-code-ide-with-scratch  :wk "Claude start")
    "a b" '(my-claude-code-ide-scratch :wk "Claude scratch buffer")
    "a i" '(claude-code-ide-insert-at-mentioned :wk "Claude insert at mentioned")
    "a s" '(my-claude-code-ide-send-region-or-prompt :wk "Claude send prompt")
    "a n" '(claude-code-ide-insert-newline :wk "Claude insert newline")
    "a 1" '(my-claude-code-ide-send-number-1 :wk "Claude send '1'")
    "a 2" '(my-claude-code-ide-send-number-2 :wk "Claude send '2'")
    "a 3" '(my-claude-code-ide-send-number-3 :wk "Claude send '3'")
    "a e" '(claude-code-ide-send-escape :wk "Claude send escape")
    "a q" '(claude-code-ide-stop :wk "Claude stop")
    "a c" '(claude-code-ide-continue :wk "Claude continue")
    "a r" '(claude-code-ide-resume :wk "Claude resume")
    "a l" '(claude-code-ide-list-sessions :wk "Claude list sessions")

    ;; (l) LSP (eglot) 操作
    "l" '(:ignore t :wk "LSP")
    "l s" '(my-eglot-start :wk "lsp start")
    )

  ;; LSP (eglot) 操作 (SPC l)
  (my-global-leader-def
    :keymaps '(eglot-mode-map)
    "l r" '(eglot-rename :wk "rename symbol")
    "l a" '(eglot-code-actions :wk "code actions")
    "l f" '(eglot-format :wk "format")
    "l R" '(eglot-reconnect :wk "lsp reconnect")
    "l q" '(eglot-shutdown :wk "lsp shutdown")
    )

  ;; === ジャンプなど (g系)
  (my-motion-leader-def
    "n" '(diff-hl-next-hunk :wk "next change")
    "p" '(diff-hl-previous-hunk :wk "prev change")
    "t" '(persp-next :wk "next workspace")
    "T" '(persp-prev :wk "prev workspace")
    )

  (my-motion-leader-def
    :keymaps '(dired-mode-map)
    "r" '(dired-subtree-revert :wk "revert dired subtree")
    )

  ;; === Lisp系の編集操作 (,)
  (my-local-leader-def
    :keymaps '(emacs-lisp-mode-map
               lisp-interaction-mode-map
               clojure-ts-mode-map
               clojure-ts-clojurescript-mode-map)
    "s" '(puni-slurp-forward :wk "slurp forward")
    "S" '(puni-slurp-backward :wk "slurp backward")
    "b" '(puni-barf-forward :wk "barf forward")
    "B" '(puni-barf-backward :wk "barf backward")
    "r" '(puni-raise :wk "raise sexp")
    "w" '(:ignore t :wk "wrap")
    "w 9" '(puni-wrap-round :wk "wrap ()")
    "w [" '(puni-wrap-square :wk "wrap []")
    "w {" '(puni-wrap-curly :wk "wrap {}")
    "w \"" '(my-wrap-symbol-with-quotes :wk "wrap \"\"")
    "d" '(:ignore t :wk "delete")
    "d w" '(puni-splice :wk "delete wrap")
    "k" '(my-puni-kill-to-end :wk "kill to sexp end")
    "h" '(eldoc :wk "eldoc")
    )

  ;; === Clojure (SPC m)
  (my-global-leader-def
    :keymaps '(clojure-ts-mode-map
               clojure-ts-clojurescript-mode-map)
    "m" '(:ignore t :wk "Clojure")
    "m i" '(cider-jack-in :wk "cider jack-in")
    "m c" '(:ignore t :wk "cider connect")
    "m c c" '(cider-connect-clj&cljs :wk "connect clj&cljs")
    "m c j" '(cider-connect-clj :wk "connect clj")
    "m c s" '(cider-connect-cljs :wk "connect cljs")
    "m q" '(cider-quit :wk "cider quit")
    "l F" '(my-clojure-lsp-clear-cache-and-restart :wk "lsp clear cache/restart")
    )

  ;; === Clojure (,)
  (my-local-leader-def
    :keymaps '(clojure-ts-mode-map
               clojure-ts-clojurescript-mode-map)
    "e" '(:ignore t :wk "Eval")
    "e e" '(cider-eval-last-sexp :wk "eval last sexp")
    "e f" '(cider-eval-dwim :wk "eval dwim")
    "e b" '(cider-eval-buffer :wk "eval bufer")
    "e n" '(cider-eval-ns-form :wk "eval ns form")
    "i" '(cider-insert-defun-in-repl :wk "insert to repl")
    "n" '(:ignore t :wk "Namespace")
    "n r" '(cider-ns-refresh :wk "cider ns refresh")
    "n s" '(cider-repl-set-ns :wk "cider ns set")
    "t" '(cider-switch-to-repl-buffer :wk "cider switch to repl"))

  ;; === Emacs Lisp (,)
  (my-local-leader-def
    :keymaps '(emacs-lisp-mode-map
               lisp-interaction-mode-map)
    "'" '(ielm :wk "ielm")
    "e" '(:ignore t :wk "Eval")
    "e e" '(eval-last-sexp :wk "eval last sexp")
    "e f" '(eval-defun :wk "eval defun")
    "e b" '(eval-buffer :wk "eval buffer")
    "i" '(my-insert-time :wk "insert time")
    "p" '(my-package-built-in-p :wk "check package built-in")
    )

  ;; === Emacs Lispの便利ヘルプ
  (general-define-key
   :keymaps 'emacs-lisp-mode-map
   :states '(normal)
   "K" 'helpful-at-point)

  ;; === Markdown
  (my-local-leader-def
    :keymaps '(gfm-mode-map)
    "," '(markdown-toggle-gfm-checkbox :wk "toggle checkbox"))

  ;; === Flymakeのエラージャンプ
  (general-define-key
   :states '(normal)
   "] ]" '(flymake-goto-next-error :wk "goto next error")
   "[ [" '(flymake-goto-prev-error :wk "goto prev error"))

  ;; === ミニバッファでパスならスラッシュまで削除
  (general-define-key
   :states '(insert)
   :keymaps '(minibuffer-mode-map)
   "C-w" 'backward-kill-sexp)

  ;; === isearch(C-sまたは/)中に単語削除はM-eの後にC-DELを押すしかない
  ;; 基本はC-hで納得しよう

  ;; === Copilot Chatでshift+enterで送信
  (general-define-key
   :keymaps 'copilot-chat-org-prompt-mode-map
   "S-<return>" 'copilot-chat-prompt-send))

;;====================================================================
;; 設定・色の細かいカスタマイズ
;;====================================================================

;; customizeメニューから変更した場合自動で追記・更新される
;; パッケージ固有の設定変数はuse-packageの:customで設定する方が宣言的で良い
;; 色の調整はここにまとめておいた方が見通しが良い(現在: catppuccin-macchiato を前提に調整)

(custom-set-variables
 ;; custom-set-variables was added by Custom.
 ;; If you edit it by hand, you could mess it up, so be careful.
 ;; Your init file should contain only one such instance.
 ;; If there is more than one, they won't work right.
 )
(custom-set-faces
 ;; custom-set-faces was added by Custom.
 ;; If you edit it by hand, you could mess it up, so be careful.
 ;; Your init file should contain only one such instance.
 ;; If there is more than one, they won't work right.
 '(diff-added ((t (:background "#3e4b4c"))))
 '(diff-refine-added ((t (:background "#586e5e"))))
 '(diff-refine-removed ((t (:background "#744d5f"))))
 '(diff-removed ((t (:background "#4c3a4c"))))
 '(ediff-current-diff-A ((t (:extend t :background "#4c3a4c"))))
 '(ediff-current-diff-B ((t (:extend t :background "#3e4b4c"))))
 '(ediff-current-diff-C ((t (:extend t :background "#4c4540"))))
 '(ediff-fine-diff-A ((t (:background "#744d5f"))))
 '(ediff-fine-diff-B ((t (:background "#586e5e"))))
 '(ediff-fine-diff-C ((t (:background "#746355"))))
 '(font-lock-comment-delimiter-face ((t (:foreground "#5ab5b0"))))
 '(font-lock-comment-face ((t (:foreground "#5ab5b0"))))
 '(match ((t (:background "#eed49f" :foreground "#1e2030"))))
 '(show-paren-match ((t (:background "#8aadf4" :foreground "#1e2030" :weight bold))))
 '(show-paren-mismatch ((t (:background "#ed8796" :foreground "#1e2030" :weight bold))))
 '(trailing-whitespace ((t (:background "#ed8796" :foreground "#ed8796")))))

;; === ローディング終了メッセージ
(message "[%s] %s" (my-display-time) "init.el loaded!")

;;; init.el ends here

キーバインド

  • SPC: space
  • DEL: backspace
  • RET: enter
  • M- : commandキーを押しながら
  • C- : ctrlキーを押しながら
  • S- : shiftキーを押しながら

基本

  • C-;: ヘルプのトリガー(C-hはbackspaceにしたいので)
  • SPC: 自分で登録したキーバインドを起動するリーダーキー(グローバル)
  • ,: 自分で登録したキーバインドを起動するリーダーキー(ローカル)

独自定義

できるだけ頭の中でニーモニックに従って思い出しやすい命名にしています。

キー 操作
SPC SPC M-xと同じ。コマンド名を与え実行
SPC ; コメントアウト・コメントイン
SPC t l 行の折り返しをトグル
SPC t f flymake(静的解析)をオン・オフ
SPC q q Emacsを終了
SPC q r Emacsを再起動
SPC f f ファイルを開く

to be continued...

Discussion