EmacsからClaude.appへ
段落を送るコマンドを作った

関連記事

1. EmacsからClaudeにプロンプトを送る

文章を書きながら、Claude.appへ指示を飛ばしたいと思っていました。
エディタとAIを行き来するのに、マウスでウィンドウを切り替えてコピペするのは手間がかかります。
Emacsにいながらカーソル位置のテキストをそのまま送れれば、もっとスムーズに使えるはずだと感じて作りました。

できあがったのは C-c C-j で発動するコマンドです。
リージョンがあればそれを、なければカーソル位置の段落をClaude.appの入力欄へ貼り付けてそのまま送信します。

1.1. 実装の概要

仕組みはシンプルで、Emacs LispとAppleScriptを組み合わせています。
Emacs側でテキストを取得してクリップボードへ入れ、do-applescript でClaude.appをアクティブにして貼り付け、Command+Enterを送ります1

使うライブラリは標準添付の subr-xseq だけです。
subr-xstring-trim などの文字列ユーティリティを提供するライブラリで、seqseq-remove でリストをフィルタリングするために使います。
どちらもEmacsに同梱されているので、外部パッケージのインストールは不要です2

2. 段落の取得ロジック

「段落」の区切りとして、空白文字だけの行を含む連続2行以上の空行を使います。
\n\n\n 以上で区切るという定義です。

(defun my/claude-current-paragraph-bounds ()
  "Return bounds of paragraph separated by two or more blank lines."
  (save-excursion
    (let (beg end)
      (setq beg
            (if (re-search-backward "\\(?:^[ \t]*$\n\\)\\{2,\\}" nil t)
                (match-end 0)
              (point-min)))
      (goto-char beg)
      (setq end
            (if (re-search-forward "\\(?:^[ \t]*$\n\\)\\{2,\\}" nil t)
                (match-beginning 0)
              (point-max)))
      (cons beg end))))Code language: Lisp (lisp)

re-search-backward でカーソルより前の直近の区切りを探し、見つからなければバッファ先頭を段落の始まりとします。
同様に re-search-forward で終端を探し、見つからなければバッファ末尾まで。
この2点の間が「段落」です3

リージョンがアクティブな場合はこのロジックをスキップして、region-beginningregion-end をそのまま使います。
Lispモードの C-c C-c に近い感覚で使えるよう、選択範囲を優先する仕様にしました。

2.1. ;; コメント行の除去

*scratch* バッファでの使用を想定しているので、;; で始まる行は送信対象から外しています。
コードのメモや日付を書き込んでいても、それがClaudeへ送られてしまうのは邪魔になるからです。

(defun my/remove-elisp-comment-lines (text)
  "Remove lines beginning with ;; from TEXT."
  (string-join
   (seq-remove
    (lambda (line)
      (string-match-p "\\`[ \t]*;;" line))
    (split-string text "\n"))
   "\n"))Code language: Lisp (lisp)

行頭に空白があっても ;; が続く行は除外します。
;; メモ のような字下げされたコメントも送られません4

3. AppleScriptによるClaude.appへの送信

テキストをクリップボードへ入れた後、AppleScriptでClaude.appをアクティブにして貼り付け、Command+Enterを送ります。

tell application "Claude" to activate
delay 0.2
tell application "System Events"
  keystroke "v" using command down
  delay 0.1
  key code 36 using command down
end tellCode language: AppleScript (applescript)

key code 36 はReturnキーのコードで、using command down でCommand+Enterになります5
delay はアプリの切り替えが完了する前にキーストロークが届いてしまうのを防ぐための短い待機です。

このAppleScriptを使うには、macOSの「システム設定 > プライバシーとセキュリティ > アクセシビリティ」でEmacsに操作権限を与える必要があります。
設定していないと System Events のキーストロークが弾かれます6

3.1. キーバインドの決め方

C-c C-j をグローバルキーに割り当てました。

C-c のあとにShiftなしの英字という組み合わせは、*scratch* 中心の使い方なら既存のキーバインドと衝突しにくいです7

C-c C-e はEmacs Lispの式評価系と干渉する可能性があり、C-c C-c はモードごとに多用されるため避けました。
j は「送る・投入する」という連想で覚えやすいという理由もあります。

(global-set-key (kbd "C-c C-j")
                #'my/send-region-or-paragraph-to-claude)Code language: Lisp (lisp)

4. 完成コード

;; Send to Claude
(require 'subr-x)
(require 'seq)

(defun my/remove-elisp-comment-lines (text)
  "Remove lines beginning with ;; from TEXT."
  (string-join
   (seq-remove
    (lambda (line)
      (string-match-p "\\`[ \t]*;;" line))
    (split-string text "\n"))
   "\n"))

(defun my/claude-current-paragraph-bounds ()
  "Return bounds of paragraph separated by two or more blank lines."
  (save-excursion
    (let (beg end)
      (setq beg
            (if (re-search-backward "\\(?:^[ \t]*$\n\\)\\{2,\\}" nil t)
                (match-end 0)
              (point-min)))
      (goto-char beg)
      (setq end
            (if (re-search-forward "\\(?:^[ \t]*$\n\\)\\{2,\\}" nil t)
                (match-beginning 0)
              (point-max)))
      (cons beg end))))

(defun my/send-region-or-paragraph-to-claude ()
  "Send active region or current paragraph to Claude.app via clipboard."
  (interactive)
  (let* ((bounds
          (if (use-region-p)
              (cons (region-beginning) (region-end))
            (my/claude-current-paragraph-bounds)))
         (raw-text
          (buffer-substring-no-properties (car bounds) (cdr bounds)))
         (text
          (string-trim
           (my/remove-elisp-comment-lines raw-text))))
    (unless (string-empty-p text)
      (kill-new text)
      (do-applescript
       "tell application \"Claude\" to activate
delay 0.2
tell application \"System Events\"
  keystroke \"v\" using command down
  delay 0.1
  key code 36 using command down
end tell"))))

(global-set-key (kbd "C-c C-j")
                #'my/send-region-or-paragraph-to-claude)Code language: Lisp (lisp)

init.el に貼るか、*scratch* で評価するだけで使い始められます8

  1. do-applescript はmacOS向けEmacsにCソースコードとして組み込まれた関数です。文字列をAppleScriptとして実行し、結果を文字列で返します。osascript コマンドをシェル経由で呼び出す方法と比べて、新たなシェルプロセスを起動しない分わずかに速く動作します。 – Calling Applescript from Emacs | Irreal
  2. subr-x はGNU Emacs 24.4以降に同梱されている追加Lisp関数ライブラリで、string-trimstring-join などを提供しています。seq はEmacs標準のシーケンス操作ライブラリで、リスト・ベクタ・文字列を統一的に扱える関数群を含んでいます。 – Modern Emacs Lisp libraries | xenodium.com
  3. コード冒頭の save-excursion は、ポイント(カーソル位置)と現在のバッファを保存し、フォームの評価が終わった後に元の状態へ戻すスペシャルフォームです。段落の範囲を探索する際に内部でカーソルを動かしても、コマンド終了後にユーザーのカーソル位置が変わらないのはこのためです。 – Excursions (GNU Emacs Lisp Reference Manual)
  4. ここで使っている seq-remove は、seq ライブラリが提供するリストのフィルタリング関数です。述語関数が真を返す要素を取り除いたリストを返します。同ライブラリには seq-filterseq-mapseq-reduce など関数型スタイルのシーケンス操作関数が揃っており、リスト・ベクタ・文字列を統一的に扱えます。 – Modern Emacs Lisp libraries | xenodium.com
  5. AppleScriptのキーコードはキーボードの物理的な位置に対応した番号です。key code 36 はReturnキーを指します。なお、テンキーのEnterキーは別の key code 76 に割り当てられており、ほとんどのアプリケーションではどちらも同じ改行として扱われます。 – Complete list of AppleScript key codes | eastmanreference.com
  6. macOS Mojave以降、他のアプリケーションへApple Eventsを送信するアプリには明示的なユーザー許可が必要です。許可は一度承認すれば以降は自動的に通りますが、アプリを再コンパイルしたり署名が変わったりすると再度求められることがあります。 – Avoiding AppleScript Security and Privacy Requests | Scripting OS X
  7. GNU Emacsのキーバインド規約では、C-c に続く単一の英字はユーザー専用として予約されており、メジャーモードやマイナーモードが使用してはならないとされています。一方、C-c に続くコントロール文字(C-c C-e など)はメジャーモード用に予約されています。 – Mastering Key Bindings in Emacs | Mastering Emacs
  8. テキストをクリップボードへ送るために使っている kill-new は、文字列をキルリングの先頭に追加する関数です。macOS上のEmacsでは interprogram-cut-function を通じてシステムクリップボードとも同期されるため、AppleScript側から Command+V で貼り付けられます。 – Low-Level Kill Ring (GNU Emacs Lisp Reference Manual)