EmacsとVtermとREPLの改善

しむどん 2025-08-02

ソフトウェア関連の技術書には、文章内にコード片が挿入されている事が多い。そのような書籍の執筆・翻訳・監訳といった仕事では、それらのコード片の動作確認も行う。このコード片の動作確認は、そこそこ悩ましい作業になる事もある。

コード片はコード全体を部分的に抜粋している事が多いため、その前後にあるであろうコードが省略されており、そのまま実行するとライブラリの読み込みが行われていなかったり、参照できない変数があり、エラーしてしまうといった事が良くある。

自著ならば原稿を自由に書きかえる事もできるが、翻訳や監訳では原著が存在するため自由に書きかえるというわけにもいかない。原著者の意図を汲み取りつつ、原著のサンプルコードから乖離し過ぎない範囲で、必要なコードを加えたり、注釈を入れたりと、読者に何が必要なのかが伝わるように、状況に応じて対応している。

コード片に含まれる問題は、抜粋によって不完全となってしまったコードというもの以外にも、元のコードに間違いがあったり、翻訳時や組版時のミスによって混入する可能性もある。そのため各フェーズでコードの確認をする事は、間違いのない文章と同じくとても大切な作業となる。

コード片が記述されるファイルの形式も、原稿の状態ではマークアップで記述されたテキストファイルだが、組版済みのものはPDFとなる。そのため、例えばソフトウェア開発で普段使用するようなツールでは非常にチェックしずらい。このようにコード片のチェックは、いくつかの込み入った「やりにくさ」がある。

このような状況の中でその都度状況を確認し必要な作業を行い、読者の皆様に品質の高い書籍をお届けできるように努めている。

今回はそのような取り組みの一環として、REPLとEmacsとの連携を強化する事にした。

僕が扱う書籍はインタプリタ型のプログラミング言語、とりわけPythonが多い。それらは大抵、Pythonのインタプリタを起動し、手動でコードを記述していく事を前提としている。ただし時折そうじゃない事もある。そのような状態のコードを、テキストファイルとPDFを行き来しつつ、動作確認しやすい環境を整える。Emacsには元々REPLの機能の補助があるけれど、もっと有効に使えるようにしたい。特にREPLの起動をDocker経由にして、Vterm上で起動するようにしたい。そこで自分用の小さなユーティリティを実装した。

コードを掲載しておく。

;; -*- lexical-binding: t; -*-

;; Copyright (C) TakesxiSximada
;;
;; Author: TakesxiSximada
;; Package-Requires: ((vterm))

;; This file is not part of GNU Emacs.

;; This file is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 2, or (at your option)
;; any later version.

;; This file is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:
;;
;; This is REPL Utility for me.

;;; Code:

(require 'vterm)

(defvar my/repl-current-buffer nil
  "This is the buffer where the REPL is running. Code will be sent to this buffer.")

;;;###autoload
(defun my/repl-select-buffer (buf)
  "Set the selected buffer as the current buffer where the REPL is running."
  (interactive "bBuffer: ")
  (setq my/repl-current-buffer buf))

;;;###autoload
(defun my/repl-send-region-to-buffer-process (beg end)
  "Send the selected region's text to the current REPL buffer."
  (interactive "r")
  (let ((code (buffer-substring-no-properties beg end)))
    (with-current-buffer my/repl-current-buffer
      ;; Send the selected region's text to the current REPL buffer, assuming vterm.
      (vterm-insert code)
      (vterm-send-return)
      (vterm-send-return))))

;;;###autoload
(defun my/repl-send-buffer-to-buffer-process ()
  "Send the entire contents of the current buffer to the REPL buffer."
  (interactive)
  (my/repl-send-region-to-buffer-process (point-min)
					 (point-max)))

(defun my/repl-pre-check-before-send (&rest args)
  "Ensure `my/repl-current-buffer` is valid before sending."
  (unless (and my/repl-current-buffer
               (buffer-live-p (get-buffer my/repl-current-buffer)))
    (call-interactively 'my/repl-select-buffer)))

(advice-add 'my/repl-send-region-to-buffer-process :before #'my/repl-pre-check-before-send)
;; delete all hooks
;; (advice-mapc 'my/repl-send-buffer-to-buffer-process #'advice-remove)

;;;###autoload
(defun my/repl-send-line-to-buffer-process ()
  "Send the current line to the REPL buffer."
  (interactive)
  (my/repl-send-region-to-buffer-process (line-beginning-position)
					 (line-end-position)))

;;;
;;; Container Utility
;;;

(defvar my/repl-docker-executable "docker"
  "Docker command")

(defvar my/repl-docker-image-list nil
  "A list of container images used to start as a REPL.")

(defun my/repl-refresh-docker-image-list ()
  "Refresh the list of container images used to start as a REPL."
  (interactive)
  (setq my/repl-docker-image-list
	(string-split
	 (shell-command-to-string "docker image list --format '{{ .Repository }}:{{ .Tag }}'"))))

;;;
;;; REPL Launch Utility
;;;

(defun my/repl-launch-with-vterm-docker-run-pre-check (&rest args)
  "Update the list of Docker images as needed before starting a container for REPL with Docker."
  (unless my/repl-docker-image-list
    (call-interactively 'my/repl-refresh-docker-image-list)))

;;;###autoload
(defun my/repl-launch-with-vterm-docker-run (&optional image cmd workdir dotenv custom-args)
  "Start a REPL process using Docker with vterm."
  (interactive
   (list
    (completing-read "Image: " my/repl-docker-image-list)
    (read-shell-command "Cmd: ")
    (read-directory-name "Dir: " default-directory nil default-directory)
    (when (yes-or-no-p "Use dotenv?:")
      (read-file-name "Dotenv: " nil ".env" t))
    (when (yes-or-no-p "Use custom args?:")
      (read-file-name "Args file: " nil ".args" t))))

  (let* ((params-env-file (if dotenv (format " --env-file %s " (shell-quote-argument dotenv)) ""))
	 (params-custom-args (if custom-args
				 (string-trim
				  (shell-command-to-string (format "head -n 1 %s" custom-args))) ""))
	 (vterm-kill-buffer-on-exit nil)
	 (vterm-buffer-name (format "*%s*: %s" image workdir))
	 (vterm-shell
	  (format "%s run -i -t --volume %s:%s --workdir %s %s %s %s %s"
		  (shell-quote-argument my/repl-docker-executable)
		  (shell-quote-argument workdir)
		  (shell-quote-argument workdir)
		  (shell-quote-argument workdir)
		  (if (string-empty-p params-env-file) "" params-env-file)
		  (if (string-empty-p params-custom-args) "" (shell-quote-argument params-custom-args))
		  (shell-quote-argument image)
		  (if (string-empty-p cmd) "" (shell-quote-argument cmd)))))
    (message "[my/repl] launti container: %s" vterm-shell)
    (vterm)))

(advice-add 'my/repl-launch-with-vterm-docker-run :before #'my/repl-launch-with-vterm-docker-run-pre-check)

(provide 'my/repl)
;;; my.el ends here

このパッケージはあくまで自分のために実装したものでありMELPAにも登録していない。これを使用したい場合は、このコードを適当なファイルにコピペして、そのEmacs Lispを評価してやれば使えるようになるだろう。もし要望があれば、パッケージを管理するコストを考慮しつつ検討しようと思う。なおEmacsユーザーの他のREPLに対する取り組みも多数あるから、ELPAやMELPAを漁ってみると良い。

このコードや文章が、何年後かに、どこかの誰かの役に立つ事つ事を願う。