[インデックス 13330] ファイルの概要
このコミットは、EmacsのGoモード(go-mode.el
)におけるgofmt
の自動整形機能の改善に関するものです。具体的には、新規ファイル作成時にgofmt
が正しく適用されない問題を修正しています。
コミット
commit d608b15db7f1e55e612f23cb9039379087a1f287
Author: Jean-Marc Eurin <jmeurin@google.com>
Date: Mon Jun 11 13:12:28 2012 -0400
misc/emacs: Fix the automatic gofmt when creating a new file.
Patching the buffer with the output from gofmt -d only works if
the file already exists. If it doesn't, replace the content with
the output of gofmt.
R=sameer
CC=golang-dev
https://golang.org/cl/6302063
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d608b15db7f1e55e612f23cb9039379087a1f287
元コミット内容
misc/emacs: Fix the automatic gofmt when creating a new file.
このコミットは、新規ファイル作成時の自動gofmt
適用を修正します。
gofmt -d
の出力でバッファにパッチを適用する方法は、ファイルが既に存在する場合にのみ機能します。ファイルが存在しない場合は、コンテンツをgofmt
の出力で置き換えます。
変更の背景
Go言語の開発において、コードのフォーマットはgofmt
というツールによって厳密に標準化されています。これにより、Goコミュニティ全体で一貫したコードスタイルが保たれ、コードレビューの効率化や可読性の向上が図られています。Emacsのgo-mode.el
は、Go言語のコードをEmacsで編集する際に、このgofmt
を自動的に実行する機能を提供しています。
しかし、既存のgo-mode.el
の実装では、新規ファイルを作成し、そのファイルにGoコードを記述して保存しようとした際に、gofmt
が期待通りに動作しないという問題がありました。これは、gofmt -d
コマンドの特性に起因します。gofmt -d
は、入力と整形後の出力の差分(diff)を生成するコマンドであり、この差分を既存のファイルに「パッチ」として適用することで、ファイルの内容を整形します。
問題は、新規ファイルの場合、比較対象となる「既存のファイル」が存在しないため、gofmt -d
が差分を生成しても、それを適用する基盤がないという点にありました。結果として、新規ファイルはgofmt
による自動整形が行われず、フォーマットが適用されないまま保存されてしまうという不具合が発生していました。
このコミットは、この特定のシナリオ(新規ファイル作成時)において、gofmt
の適用方法を切り替えることで、問題を解決することを目的としています。
前提知識の解説
gofmt
gofmt
は、Go言語のソースコードを標準的なスタイルに自動的にフォーマットするツールです。Go言語のツールチェインに標準で含まれており、Go言語のコードベース全体で一貫したスタイルを強制するために広く利用されています。
gofmt
: 標準入力からGoコードを読み込み、整形されたコードを標準出力に出力します。ファイル名を引数に与えると、そのファイルを直接整形して上書きします。gofmt -d
:diff
形式で整形前後の差分を出力します。このオプションは、変更内容を確認したり、パッチとして適用したりする際に便利です。
Emacsのgo-mode.el
go-mode.el
は、EmacsエディタでGo言語のコードを編集するためのメジャーモードです。Go言語のシンタックスハイライト、インデント、そしてgofmt
との連携による自動コード整形などの機能を提供します。
shell-command-on-region
: Emacs Lispの関数で、指定されたリージョン(範囲)のテキストを外部コマンドの標準入力に渡し、そのコマンドの標準出力や標準エラー出力をEmacsバッファにキャプチャする機能を提供します。このコミットでは、gofmt
コマンドの実行に利用されています。diff-mode
: Emacsのモードの一つで、差分ファイル(diff形式の出力)を表示し、その差分を現在のバッファに適用する機能を提供します。gofmt -d
の出力はdiff
形式であるため、既存のファイルに対して整形結果を適用する際にdiff-mode
の機能が利用されていました。file-exists-p
: Emacs Lispの関数で、指定されたファイルが存在するかどうかを判定します。このコミットでは、現在のバッファが新規ファイルであるか既存ファイルであるかを判断するために使用されています。
パッチ適用とバッファの置き換え
- パッチ適用:
gofmt -d
の出力は、diff
コマンドの出力と同様に、元のファイルと変更後のファイルの差分を記述したものです。これを既存のファイルに適用することで、元のファイルを変更後のファイルの状態に更新できます。Emacsのdiff-mode
は、このパッチ適用をEmacsバッファ内で実現する機能を持っています。 - バッファの置き換え: 新規ファイルの場合、パッチを適用する「元のファイル」が存在しないため、差分を適用するのではなく、
gofmt
の整形結果を直接バッファの内容として置き換える必要があります。これは、バッファの内容を一度クリアし、整形済みのコードを挿入することで実現されます。
技術的詳細
このコミットの主要な変更点は、gofmt
の実行結果をEmacsバッファに適用するロジックに条件分岐を追加したことです。
- ファイル存在チェック:
(file-exists-p filename)
というEmacs Lisp関数を使用して、現在編集中のファイルが既にディスク上に存在するかどうかを判定します。newfile
という変数に、ファイルが存在しない場合にt
(真)、存在する場合にnil
(偽)が設定されます。
gofmt
コマンドの選択:- もし
newfile
がt
(新規ファイル)であれば、gofmt
コマンドには-d
オプションを付けずに実行します。これにより、整形されたコード全体が標準出力に出力されます。 - もし
newfile
がnil
(既存ファイル)であれば、従来通りgofmt -d
コマンドを実行し、整形前後の差分が標準出力に出力されます。 - この選択は、
flag
という変数に""
(空文字列)または" -d"
を設定し、concat "gofmt" flag
としてコマンド文字列を構築することで実現されています。
- もし
- 整形結果の適用方法の分岐:
gofmt
コマンドが成功した場合(shell-command-on-region
の戻り値が0)、整形結果をEmacsバッファに適用します。ここでも、newfile
の値によって処理が分岐します。- 新規ファイルの場合 (
newfile
がt
):- 新しく定義された
gofmt-replace-buffer
関数が呼び出されます。 - この関数は、現在のバッファ(
srcbuf
)の内容を完全に消去し(erase-buffer
)、gofmt
の出力が格納されている一時バッファ(patchbuf
)の内容をすべて挿入します(insert-buffer-substring patchbuf
)。これにより、新規ファイルの内容が整形済みのコードで直接置き換えられます。
- 新しく定義された
- 既存ファイルの場合 (
newfile
がnil
):- 従来通り
gofmt-apply-patch
関数が呼び出されます。 - この関数は、
gofmt -d
によって生成された差分を、diff-mode
の機能を利用して現在のバッファにパッチとして適用します。これにより、Emacsのundo履歴を壊すことなく、効率的に整形が適用されます。
- 従来通り
- カーソル位置とマークの復元: どちらのケースでも、
gofmt
適用後にユーザーのカーソル位置(old-point
)とマーク(old-mark
)を可能な限り元の位置に復元するロジックが含まれています。これは、ユーザーの編集体験を損なわないための重要な配慮です。
また、このコミットでは、gofmt-apply-patch
関数からカーソル位置とマークの復元ロジックが削除され、呼び出し元(gofmt-buffer
)に移動しています。これは、gofmt-replace-buffer
とgofmt-apply-patch
の両方で共通してカーソル位置とマークの復元を行うため、コードの重複を避けるためのリファクタリングです。
コアとなるコードの変更箇所
misc/emacs/go-mode.el
ファイルのgofmt-buffer
関数内が主な変更点です。
--- a/misc/emacs/go-mode.el
+++ b/misc/emacs/go-mode.el
@@ -777,43 +777,59 @@ Replace the current buffer on success; display errors on failure.\"
(save-restriction
(let (deactivate-mark)\
(widen)\
- (if (= 0 (shell-command-on-region (point-min) (point-max) \"gofmt -d\"\
- patchbuf nil errbuf))\
- ; gofmt succeeded: apply patch hunks.
- (progn
- (kill-buffer errbuf)\
- (gofmt-apply-patch filename srcbuf patchbuf)\
- (set-window-configuration currconf))\
+ ; If this is a new file, diff-mode can\'t apply a
+ ; patch to a non-exisiting file, so replace the buffer
+ ; completely with the output of \'gofmt\'.
+ ; If the file exists, patch it to keep the \'undo\' list happy.
+ (let* ((newfile (not (file-exists-p filename)))\
+ (flag (if newfile \"\" \" -d\")))\
+ (if (= 0 (shell-command-on-region (point-min) (point-max)\
+ (concat \"gofmt\" flag)\
+ patchbuf nil errbuf))\
+ ; gofmt succeeded: replace buffer or apply patch hunks.
+ (let ((old-point (point))\
+ (old-mark (mark t)))\
+ (kill-buffer errbuf)\
+ (if newfile\
+ ; New file, replace it (diff-mode won\'t work)
+ (gofmt-replace-buffer srcbuf patchbuf)\
+ ; Existing file, patch it
+ (gofmt-apply-patch filename srcbuf patchbuf))\
+ (goto-char (min old-point (point-max)))\
+ ;; Restore the mark and point
+ (if old-mark (push-mark (min old-mark (point-max)) t))\
+ (set-window-configuration currconf))\
;; gofmt failed: display the errors
- (gofmt-process-errors filename errbuf)))))\
+ (gofmt-process-errors filename errbuf))))))\
;; Collapse any window opened on outbuf if shell-command-on-region
;; displayed it.\
(delete-windows-on patchbuf)))\
(kill-buffer patchbuf))))\
+(defun gofmt-replace-buffer (srcbuf patchbuf)\
+ (with-current-buffer srcbuf\
+ (erase-buffer)\
+ (insert-buffer-substring patchbuf)))\
+\
(defconst gofmt-stdin-tag \"<standard input>\")\
(defun gofmt-apply-patch (filename srcbuf patchbuf)\
(require \'diff-mode)\
;; apply all the patch hunks and restore the mark and point
- (let ((old-point (point))\
- (old-mark (mark t)))\
- (with-current-buffer patchbuf\
- (let ((filename (file-name-nondirectory filename))\
- (min (point-min)))\
- (replace-string gofmt-stdin-tag filename nil min (point-max))\
- (replace-regexp \"^--- /tmp/gofmt[0-9]*\" (concat \"--- /tmp/\" filename)\
- nil min (point-max)))\
- (condition-case nil\
- (while t\
- (diff-hunk-next)\
- (diff-apply-hunk))\
- ;; When there\'s no more hunks, diff-hunk-next signals an error, ignore it\
- (error nil)))\
- (goto-char (min old-point (point-max)))\
- (if old-mark (push-mark (min old-mark (point-max)) t))))\
+ (with-current-buffer patchbuf\
+ (let ((filename (file-name-nondirectory filename))\
+ (min (point-min)))\
+ (replace-string gofmt-stdin-tag filename nil min (point-max))\
+ (replace-regexp \"^--- /tmp/gofmt[0-9]*\" (concat \"--- /tmp/\" filename)\
+ nil min (point-max)))\
+ (condition-case nil\
+ (while t\
+ (diff-hunk-next)\
+ (diff-apply-hunk))\
+ ;; When there\'s no more hunks, diff-hunk-next signals an error, ignore it\
+ (error nil))))\
(defun gofmt-process-errors (filename errbuf)\
;; Convert the gofmt stderr to something understood by the compilation mode.\
コアとなるコードの解説
gofmt-buffer
関数内の変更
-
newfile
変数の導入:(let* ((newfile (not (file-exists-p filename))) (flag (if newfile "" " -d")))
file-exists-p
関数を使って、filename
で指定されたファイルがディスク上に存在するかどうかをチェックします。その結果を反転させ(not
)、newfile
変数に格納します。newfile
がt
であれば新規ファイル、nil
であれば既存ファイルです。flag
変数には、newfile
がt
の場合は空文字列(gofmt
をそのまま実行)、nil
の場合は" -d"
(gofmt -d
を実行)が設定されます。 -
gofmt
コマンドの実行:(if (= 0 (shell-command-on-region (point-min) (point-max) (concat "gofmt" flag) patchbuf nil errbuf))
shell-command-on-region
を使ってgofmt
を実行します。ここで、先ほど決定したflag
がgofmt
コマンドに連結されます。これにより、新規ファイルの場合はgofmt
が、既存ファイルの場合はgofmt -d
が実行されるようになります。 -
整形結果の適用ロジックの分岐:
(if newfile ; New file, replace it (diff-mode won't work) (gofmt-replace-buffer srcbuf patchbuf) ; Existing file, patch it (gofmt-apply-patch filename srcbuf patchbuf))
newfile
の値に基づいて、整形結果の適用方法を分岐します。newfile
がt
(新規ファイル)の場合、新しく定義されたgofmt-replace-buffer
関数を呼び出し、バッファの内容を直接置き換えます。newfile
がnil
(既存ファイル)の場合、既存のgofmt-apply-patch
関数を呼び出し、差分をパッチとして適用します。
-
カーソル位置とマークの復元ロジックの移動:
gofmt-apply-patch
関数から、old-point
とold-mark
を保存し、整形後に復元するロジックが削除され、gofmt-buffer
関数内の共通の場所に移されました。これにより、新規ファイルと既存ファイルのどちらのケースでも、整形後にカーソル位置とマークが正しく復元されることが保証されます。
gofmt-replace-buffer
関数の追加
(defun gofmt-replace-buffer (srcbuf patchbuf)
(with-current-buffer srcbuf
(erase-buffer)
(insert-buffer-substring patchbuf)))
この新しい関数は、新規ファイルの場合に呼び出されます。
with-current-buffer srcbuf
:srcbuf
(整形対象のGoコードが書かれているバッファ)を一時的に現在のバッファにします。erase-buffer
:srcbuf
の現在の内容をすべて消去します。insert-buffer-substring patchbuf
:patchbuf
(gofmt
の整形結果が格納されている一時バッファ)の内容をすべてsrcbuf
に挿入します。
これにより、新規ファイルの内容がgofmt
によって整形されたコードで完全に置き換えられます。
gofmt-apply-patch
関数の変更
gofmt-apply-patch
関数からは、old-point
とold-mark
を保存し、整形後に復元するロジックが削除されました。このロジックはgofmt-buffer
関数に移動し、gofmt-replace-buffer
とgofmt-apply-patch
の両方で共通して処理されるようになりました。これにより、コードの重複が解消され、保守性が向上しています。
関連リンク
参考にした情報源リンク
- golang/go GitHubリポジトリ
- Emacs Lisp Reference Manual
- gofmt -d new file - Google Search (この検索は、
gofmt -d
が新規ファイルで動作しないという一般的な知識を確認するために行われました。) - golang.org/cl/6302063 (コミットメッセージに記載されているGoのコードレビューシステムへのリンク)