Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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バッファに適用するロジックに条件分岐を追加したことです。

  1. ファイル存在チェック: (file-exists-p filename)というEmacs Lisp関数を使用して、現在編集中のファイルが既にディスク上に存在するかどうかを判定します。
    • newfileという変数に、ファイルが存在しない場合にt(真)、存在する場合にnil(偽)が設定されます。
  2. gofmtコマンドの選択:
    • もしnewfilet(新規ファイル)であれば、gofmtコマンドには -dオプションを付けずに実行します。これにより、整形されたコード全体が標準出力に出力されます。
    • もしnewfilenil(既存ファイル)であれば、従来通りgofmt -dコマンドを実行し、整形前後の差分が標準出力に出力されます。
    • この選択は、flagという変数に""(空文字列)または" -d"を設定し、concat "gofmt" flagとしてコマンド文字列を構築することで実現されています。
  3. 整形結果の適用方法の分岐:
    • gofmtコマンドが成功した場合(shell-command-on-regionの戻り値が0)、整形結果をEmacsバッファに適用します。ここでも、newfileの値によって処理が分岐します。
    • 新規ファイルの場合 (newfilet):
      • 新しく定義されたgofmt-replace-buffer関数が呼び出されます。
      • この関数は、現在のバッファ(srcbuf)の内容を完全に消去し(erase-buffer)、gofmtの出力が格納されている一時バッファ(patchbuf)の内容をすべて挿入します(insert-buffer-substring patchbuf)。これにより、新規ファイルの内容が整形済みのコードで直接置き換えられます。
    • 既存ファイルの場合 (newfilenil):
      • 従来通りgofmt-apply-patch関数が呼び出されます。
      • この関数は、gofmt -dによって生成された差分を、diff-modeの機能を利用して現在のバッファにパッチとして適用します。これにより、Emacsのundo履歴を壊すことなく、効率的に整形が適用されます。
  4. カーソル位置とマークの復元: どちらのケースでも、gofmt適用後にユーザーのカーソル位置(old-point)とマーク(old-mark)を可能な限り元の位置に復元するロジックが含まれています。これは、ユーザーの編集体験を損なわないための重要な配慮です。

また、このコミットでは、gofmt-apply-patch関数からカーソル位置とマークの復元ロジックが削除され、呼び出し元(gofmt-buffer)に移動しています。これは、gofmt-replace-buffergofmt-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変数に格納します。newfiletであれば新規ファイル、nilであれば既存ファイルです。 flag変数には、newfiletの場合は空文字列(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を実行します。ここで、先ほど決定したflaggofmtコマンドに連結されます。これにより、新規ファイルの場合は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の値に基づいて、整形結果の適用方法を分岐します。

    • newfilet(新規ファイル)の場合、新しく定義されたgofmt-replace-buffer関数を呼び出し、バッファの内容を直接置き換えます。
    • newfilenil(既存ファイル)の場合、既存のgofmt-apply-patch関数を呼び出し、差分をパッチとして適用します。
  • カーソル位置とマークの復元ロジックの移動: gofmt-apply-patch関数から、old-pointold-markを保存し、整形後に復元するロジックが削除され、gofmt-buffer関数内の共通の場所に移されました。これにより、新規ファイルと既存ファイルのどちらのケースでも、整形後にカーソル位置とマークが正しく復元されることが保証されます。

gofmt-replace-buffer関数の追加

(defun gofmt-replace-buffer (srcbuf patchbuf)
  (with-current-buffer srcbuf
    (erase-buffer)
    (insert-buffer-substring patchbuf)))

この新しい関数は、新規ファイルの場合に呼び出されます。

  1. with-current-buffer srcbuf: srcbuf(整形対象のGoコードが書かれているバッファ)を一時的に現在のバッファにします。
  2. erase-buffer: srcbufの現在の内容をすべて消去します。
  3. insert-buffer-substring patchbuf: patchbufgofmtの整形結果が格納されている一時バッファ)の内容をすべてsrcbufに挿入します。

これにより、新規ファイルの内容がgofmtによって整形されたコードで完全に置き換えられます。

gofmt-apply-patch関数の変更

gofmt-apply-patch関数からは、old-pointold-markを保存し、整形後に復元するロジックが削除されました。このロジックはgofmt-buffer関数に移動し、gofmt-replace-buffergofmt-apply-patchの両方で共通して処理されるようになりました。これにより、コードの重複が解消され、保守性が向上しています。

関連リンク

参考にした情報源リンク