🕶️

Emacsを旧版Boostnoteみたいにする

2023/02/11に公開

Emacsを旧版Boostnoteみたいにカスタマイズする

4年ほど旧版のBoostnoteを使ってきたのだが、旧版は開発が止まったことと、ファイルが千件を超えてくると動作が遅くなってきた。他のエディタに乗り換えようかと思ったがどれもしっくりこなかったので、Emacsをカスタマイズして旧版Boostnoteみたいにすることにした。

カスタマイズをしながら半年ほど使ってかなりしっくりきたので、検討したことや詰まったところをまとめておく。旧版Boostnoteに惚れ込んでいた同士たちはよかったら参考にしてください。最終的なinit.elはここです。

https://github.com/roronya/dotfiles/blob/main/emacs/init.el

最終的に作ったもの

ファイルツリーが開閉できる2カラムのシンプルなエディタになった。以下のように使い心地を作り込んでいる。

  • 名前を決めずにファイルを作れる。保存時にMarkdownのh1を見てファイル名を勝手に付ける。
  • YAMLヘッダを書くとZenn用に記事として保存されGithubにpushするとZennに公開できる。
  • アルファベットのまま日本語を検索できる。

Image from Gyazo

mac用のEmacsのどれを使うか

mac用のemacsバイナリは色々あるが、EMP版と呼ばれているやつが日本語対応されており使い勝手がよかった。

https://github.com/railwaycat/homebrew-emacsmacport

(他のemacsバイナリについては以下の記事が詳しい。 https://monologu.com/select-emacs-for-mac/ )

EMP版はインストールもbrewでできて簡単。

brew tap railwaycat/emacsmacport
brew install --cask emacs-mac

さらにEMP版なら以下の設定を入れておけば、M-xやC-xを打ったときにOSのIMEを自動で英数に切り替えてくれる。

(mac-auto-ascii-mode 1)

名前を決めずにファイルを作る

旧版Boostnoteは ⌘-n で空ノートが作成されて、Markdownでh1を書くとそれが勝手に見出しとなった。この挙動が気持ちよかったので、同様の挙動をEmacsで再現するために以下のような処理を書いた。

  1. ファイル作成時に日付でファイルを作る。 e.g. 2023_0211.md
  2. 保存時にMarkdownでh1を書いてある行を見つけてその名前でファイル名を上書きする。 e.g. 2023_0211_Emacsを旧Boostnoteみたいにカスタマイズする.md

prefixに保存時の日付を入れることで、ファイルツリーで一覧にしたときに更新順の降順に一覧にできる。

Image from Gyazo

具体的なコードはこんな感じ。 結構むりやりではある。

(setq my/project-root "~/Documents/mynote/notes/")
(defun my/format-current-time-string () (format-time-string "%Y_%m%d" (current-time)))
(defun my/generate-junk-filename () (concat (my/format-current-time-string) ".md")) ;; => e.g. 2022_0808.md
(defun my/open-file () (interactive) (find-file (concat my/project-root (my/generate-junk-filename))))
;; FIXME: h1のヘッダがファイル内に1つで1文字以上の長さを持っていないとエラーになる
(defun my/extract-header ()  (substring (shell-command-to-string (concat "grep \"^# \" \"" (buffer-name) "\"")) 2 -1))
;; ヘッダに日付をつけた文字列を生成する
(defun my/generate-named-filename ()
  (concat (my/format-current-time-string) "_" (my/extract-header) ".md"))
;; ヘッダが無いファイルは日付で名前を付ける
(defun my/determine-filename ()
  (if (string= (my/extract-header) "")
      (my/generate-junk-filename) (my/generate-named-filename)))

;; 保存時にjunk filenameをmarkdownのh1で上書きする
(defun my/rename-file-and-buffer ()
  (let ((name (buffer-name))
	(filename (buffer-file-name))
	(new-name (my/determine-filename)))
    (if (not filename)
	(message "Buffer '%s' is not visiting a file!" name)
      (if (get-buffer new-name)
	  (message "A buffer named '%s' already exists!" new-name)
	(progn
	  (rename-file filename new-name 1)
	  (rename-buffer new-name)
	  (set-visited-file-name new-name)
	  (set-buffer-modified-p nil))))))

保存時に処理を仕込むにはadd-hookを書けば良い。この例はファイル名の書き換えしかしてないけど、実際には my/do-after-save という関数に保存時の処理を色々書いている。

(defun my/do-after-save ()
  (interactive)
  (when (eq major-mode 'markdown-mode)
    (my/rename-file-and-buffer)))
(add-hook 'after-save-hook 'my/do-after-save)

Zenn用の記事を作りやすくする

YAMLヘッダが書かれている場合は保存時にZenn公開用ディレクトリに記事をコピーするようにしてある。ZennにGithubのディレクトリを登録しておけば、git pushでZennに公開できる。結構無理のある記述だけど、このような保存時に特殊な挙動を起動できるのが、EmacsでMarkdownエディタを作り込む楽しさだなあと思った。

(setq my/articles-root "~/Documents/mynote/articles/")
;; 保存時にzennの指定があったらarticlesにコピーする
(defun my/zenn-articles? ()
  (beginning-of-buffer)
  (string= "---" (buffer-substring-no-properties 1 4)))
;; slugを取得する
(defun my/get-zenn-slug ()
  (beginning-of-buffer)
  ;; いい方法が思いつかなかったので、ファイル先頭からslugの場所までの文字数を数えている
  (buffer-substring-no-properties 11 25))
(defun my/move-note-to-articles ()
  ;; my/zenn-articles?とmy/get-senn-slugでカーソル場所が動くので保存しておいて後で戻す
  (point-to-register 'r)
  (if (my/zenn-articles?)
      (let ((oldname (buffer-file-name))
	    (newname (concat my/articles-root (my/get-zenn-slug) ".md")))
	(copy-file oldname newname t)))
  (jump-to-register 'r))

my/move-note-to-articles を前述した my/do-after-save に書いておけば保存時に記事がZenn用のディレクトリにコピーされる。

アルファベットのまま日本語で記事を検索する

これは何を言っているんだという感じがするんだけど、つまり画像の様に 仙台 の記事について調べるときに sendai と打てるということ(個人的な旅行の予定がさらされて恥ずかしいのだが丁度良い例がなかった)。cmigemoで実現できる。

Image from Gyazo

;; 日本語をローマ字で検索する
(use-package migemo
  :init
  (setq migemo-directory "/opt/homebrew/Cellar/cmigemo/HEAD-e0f6145/share/migemo/utf-8") ;; 人によって違うのでそれぞれで設定する必要がある
  (setq migemo-command (executable-find "cmigemo"))
  (setq migemo-options '("-q" "--emacs" "--nonewline"))
  (setq migemo-dictionary (expand-file-name "migemo-dict" migemo-directory))
  (setq migemo-coding-system 'utf-8-unix)
  (setq migemo-user-dictionary nil)
  (setq migemo-regex-dictionary nil)
  (add-hook 'after-init-hook #'migemo-init))

;; 検索結果を表示するミニバッファの設定
(use-package vertico
  :init (vertico-mode)
  :config
  (setq vertico-count 30))
(use-package orderless
  :config
  (setq completion-styles '(orderless)))
(use-package consult
  :init
  (defun consult--migemo-regexp-compiler (input type ignore-case)
    (setq input (mapcar #'migemo-get-pattern (consult--split-escaped input)))
    (cons (mapcar (lambda (x) (consult--convert-regexp x type)) input)
          (when-let (regexps (seq-filter #'consult--valid-regexp-p input))
            (apply-partially #'consult--highlight-regexps regexps ignore-case))))

  :config
  (setq consult--regexp-compiler #'consult--migemo-regexp-compiler)
  ;; consultはarticles配下だけを検索する
  (setq consult-project-function (lambda (_) my/project-root))
  :bind
  ("M-f" . consult-ripgrep))

このあとやりたい機能拡張

半年ほど使い込んでちょっとずつ機能を付け足してきてかなり便利に使えている。特にZenn用の拡張は便利で、自分用のメモに少し手入れすればインターネットにアウトプットできるので、多少はアウトプットの回数が増えたかなと思う。

しかし、まだまだ旧版Boostnoteには劣る。特にMarkdownの機能は足りていないと思う。旧版BoostnoteではPlantUMLを埋め込むことができた。この機能はmarkdown-modeでmarkdown compilerにpandocを指定して、pandocを拡張すれば実現できそうだ。

https://qiita.com/ktz_alias/items/0d7da027a854f0b20fcd

GitHubで編集を提案

Discussion