[インデックス 13042] ファイルの概要
このコミットは、Emacsのgo-mode.el
ファイルに対する変更であり、gofmt
コマンド実行後にEmacsのウィンドウ位置(スクロール位置)が予期せず移動してしまう問題を解決することを目的としています。具体的には、gofmt
が正常に実行された際に、バッファの内容が更新された後でも、ユーザーが以前見ていたウィンドウの開始位置を正確に復元するように改善されています。これにより、コードの整形後に視覚的な連続性が保たれ、ユーザーエクスペリエンスが向上します。
コミット
commit 9d063816ef42f90ae0af001f3348fc535fdef753
Author: Jean-Marc Eurin <jmeurin@google.com>
Date: Mon May 7 11:46:01 2012 -0400
misc/emacs: Restore the window position after a successful gofmt.
This adds restoring the window position so that the buffer doesn't jump around after the erase/copy.
R=sameer
CC=golang-dev
https://golang.org/cl/5981055
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9d063816ef42f90ae0af001f3348fc535fdef753
元コミット内容
misc/emacs: Restore the window position after a successful gofmt.
This adds restoring the window position so that the buffer doesn't jump around after the erase/copy.
変更の背景
Go言語の開発において、gofmt
はコードの自動整形を行うための標準ツールです。Emacsのgo-mode
は、このgofmt
をEmacs内で実行し、現在のバッファの内容を整形されたコードで置き換える機能を提供しています。しかし、従来のgofmt
実行後の処理では、バッファの内容がerase-buffer
(バッファの全消去)とinsert-buffer-substring
(整形済みコードの挿入)によって更新される際に、Emacsのウィンドウのスクロール位置(window-start
)がリセットされてしまう問題がありました。
この問題は、ユーザーがコードを整形した後に、以前見ていたコードの箇所が画面から外れてしまい、手動でスクロールし直す必要が生じるという、軽微ながらも煩わしいユーザーエクスペリエンスの低下を引き起こしていました。特に、大規模なファイルや、頻繁にgofmt
を使用する開発者にとっては、この「ジャンプ」現象は生産性を阻害する要因となっていました。
このコミットは、この問題を解決し、gofmt
実行後もユーザーの視点が維持されるようにすることで、EmacsにおけるGo開発のワークフローをよりスムーズにすることを目的としています。
前提知識の解説
このコミットを理解するためには、以下の概念についての知識が役立ちます。
- Emacs (GNU Emacs): 高度にカスタマイズ可能なテキストエディタであり、統合開発環境 (IDE) としても機能します。Emacs Lisp (Elisp) というプログラミング言語で拡張されており、ユーザーはElispを使ってEmacsの動作を細かく制御できます。
- Emacs Lisp (Elisp): Emacsの拡張機能や設定を記述するためのプログラミング言語です。Emacsのほぼ全ての機能はElispで実装されており、ユーザーはElispコードを記述することでEmacsを自由にカスタマイズできます。
go-mode.el
: EmacsでGo言語のコードを編集するためのメジャーモード(特定のプログラミング言語に対応した編集モード)を提供するElispファイルです。シンタックスハイライト、インデント、gofmt
との連携などの機能を提供します。gofmt
: Go言語の公式なコード整形ツールです。Goのソースコードを標準的なスタイルに自動的に整形します。Go開発においては、gofmt
によって整形されたコードが標準とされており、多くのプロジェクトでCI/CDパイプラインに組み込まれています。- バッファ (Buffer): Emacsがテキストを保持するためのメモリ上の領域です。ファイルの内容はバッファに読み込まれ、編集が行われます。
- ウィンドウ (Window): Emacsのフレーム(GUIウィンドウ)内でバッファの内容を表示する領域です。一つのフレーム内に複数のウィンドウを分割して表示することができます。
- ポイント (Point): バッファ内のカーソル位置を示す概念です。
- マーク (Mark): Emacsでリージョン(選択範囲)を指定する際に使用される、もう一つの位置を示す概念です。ポイントとマークの間がリージョンとなります。
current-window-configuration
: 現在のウィンドウの配置、サイズ、表示バッファ、スクロール位置などの状態を保存するElisp関数です。set-window-configuration
:current-window-configuration
で保存した状態を復元するElisp関数です。window-start
: ウィンドウの表示領域の先頭がバッファのどの位置から始まるかを示す概念です。スクロール位置を決定します。set-window-start
: 指定されたウィンドウのwindow-start
を設定するElisp関数です。これにより、ウィンドウのスクロール位置をプログラム的に制御できます。shell-command-on-region
: Emacsのリージョン(選択範囲)またはバッファ全体の内容を外部シェルコマンドの標準入力に渡し、その出力を別のバッファに書き込むElisp関数です。gofmt
の実行に利用されています。erase-buffer
: 現在のバッファの内容を全て消去するElisp関数です。insert-buffer-substring
: 別のバッファの内容の一部または全体を現在のバッファに挿入するElisp関数です。
技術的詳細
このコミットの核心は、gofmt
が成功した際に、バッファの内容を更新する前に現在のウィンドウのスクロール位置を保存し、内容更新後にその位置を復元するというロジックの追加です。
変更前のコードでは、gofmt
が成功した場合、old-mark
とold-point
(カーソル位置とマーク位置)は保存されていましたが、ウィンドウのスクロール位置(window-start
)は保存されていませんでした。そのため、erase-buffer
とinsert-buffer-substring
によってバッファの内容が完全に置き換えられると、Emacsはウィンドウの表示開始位置をリセットしてしまい、結果としてユーザーの視点が失われることになりました。
このコミットでは、以下の変更が加えられています。
old-start
変数の追加:gofmt
成功時のlet
バインディングに、old-start (window-start)
が追加されました。これにより、gofmt
実行前の現在のウィンドウの表示開始位置がold-start
変数に保存されます。set-window-start
の呼び出し:insert-buffer-substring
によるバッファ内容の更新後、set-window-configuration currconf
の後にset-window-start (selected-window) (min old-start (point-max))
が追加されました。set-window-configuration currconf
は、ウィンドウの配置自体を復元しますが、個々のウィンドウのスクロール位置までは復元しません。set-window-start (selected-window) (min old-start (point-max))
は、現在選択されているウィンドウ(selected-window
)の表示開始位置を、保存しておいたold-start
に設定します。min old-start (point-max)
としているのは、old-start
がバッファの末尾を超えていた場合に、バッファの末尾に調整するためです。
これらの変更により、gofmt
が成功してバッファの内容が更新された後でも、ユーザーが以前見ていたスクロール位置が正確に復元されるようになり、視覚的なジャンプが解消されました。
コアとなるコードの変更箇所
misc/emacs/go-mode.el
ファイルの gofmt-buffer
関数内。
--- a/misc/emacs/go-mode.el
+++ b/misc/emacs/go-mode.el
@@ -743,34 +743,37 @@ Replace the current buffer on success; display errors on failure."
(let ((currconf (current-window-configuration)))
(let ((srcbuf (current-buffer)))
(with-temp-buffer
- (let ((outbuf (current-buffer))
- (errbuf (get-buffer-create "*Gofmt Errors*"))
- (coding-system-for-read 'utf-8) ;; use utf-8 with subprocesses
- (coding-system-for-write 'utf-8))\n- (with-current-buffer errbuf (erase-buffer))\n- (with-current-buffer srcbuf\n- (save-restriction\n- (let (deactivate-mark)\n- (widen)\n- (if (= 0 (shell-command-on-region (point-min) (point-max) "gofmt"\n- outbuf nil errbuf))\n- ;; restore window config\n- ;; gofmt succeeded: replace the current buffer with outbuf,\n- ;; restore the mark and point, and discard errbuf.\n- (let ((old-mark (mark t)) (old-point (point)))\n- (set-window-configuration currconf)\n- (erase-buffer)\n- (insert-buffer-substring outbuf)\n- (goto-char (min old-point (point-max)))\n- (if old-mark (push-mark (min old-mark (point-max)) t))\n- (kill-buffer errbuf))\n-\n- ;; gofmt failed: display the errors\n- (display-buffer errbuf)))))\n-\n- ;; Collapse any window opened on outbuf if shell-command-on-region\n- ;; displayed it.\n- (delete-windows-on outbuf))))))\n+ (let ((outbuf (current-buffer))\n+ (errbuf (get-buffer-create "*Gofmt Errors*"))\n+ (coding-system-for-read 'utf-8) ;; use utf-8 with subprocesses\n+ (coding-system-for-write 'utf-8))\n+ (with-current-buffer errbuf (erase-buffer))\n+ (with-current-buffer srcbuf\n+ (save-restriction\n+ (let (deactivate-mark)\n+ (widen)\n+ (if (= 0 (shell-command-on-region (point-min) (point-max) "gofmt"\n+ outbuf nil errbuf))\n+ ;; restore window config\n+ ;; gofmt succeeded: replace the current buffer with outbuf,\n+ ;; restore the mark and point, and discard errbuf.\n+ (let ((old-mark (mark t))\n+ (old-point (point))\n+ (old-start (window-start)))\n+ (erase-buffer)\n+ (insert-buffer-substring outbuf)\n+ (set-window-configuration currconf)\n+ (set-window-start (selected-window) (min old-start (point-max)))\n+ (goto-char (min old-point (point-max)))\n+ (if old-mark (push-mark (min old-mark (point-max)) t))\n+ (kill-buffer errbuf))\n+\n+ ;; gofmt failed: display the errors\n+ (display-buffer errbuf)))))\n+\n+ ;; Collapse any window opened on outbuf if shell-command-on-region\n+ ;; displayed it.\n+ (delete-windows-on outbuf))))))\n```
## コアとなるコードの解説
変更は、`gofmt-buffer`関数内の`gofmt`が成功した場合の処理ブロックに集中しています。
1. **`old-start (window-start)` の追加**:
```elisp
(let ((old-mark (mark t))
(old-point (point))
(old-start (window-start))) ; <-- この行が追加
```
`gofmt`を実行する前に、現在のウィンドウの表示開始位置(スクロール位置)を`window-start`関数で取得し、`old-start`という新しい変数に保存しています。これにより、後でこの位置を復元できるようになります。
2. **`set-window-start` の呼び出し**:
```elisp
(erase-buffer)
(insert-buffer-substring outbuf)
(set-window-configuration currconf)
(set-window-start (selected-window) (min old-start (point-max))) ; <-- この行が追加
(goto-char (min old-point (point-max)))
```
バッファの内容が`erase-buffer`と`insert-buffer-substring`によって新しい整形済みコードで置き換えられ、さらに`set-window-configuration`でウィンドウの全体的な配置が復元された後、`set-window-start`関数が呼び出されます。
* `selected-window`は現在アクティブなウィンドウを指します。
* `min old-start (point-max)`は、保存しておいた`old-start`の位置を、バッファの実際の最大ポイント(末尾)と比較し、もし`old-start`がバッファの末尾を超えていた場合は、バッファの末尾に調整します。これにより、無効なスクロール位置が設定されるのを防ぎます。
この行の追加により、`gofmt`実行後のバッファ内容の変更にもかかわらず、ユーザーの視点が以前のスクロール位置に正確に復元されるようになりました。
これらの変更により、`gofmt`によるコード整形がよりシームレスな体験となり、開発者はコードの整形後に手動でスクロール位置を調整する手間が省けるようになりました。
## 関連リンク
* Go CL 5981055: [https://golang.org/cl/5981055](https://golang.org/cl/5981055)
## 参考にした情報源リンク
* GNU Emacs Manual: [https://www.gnu.org/software/emacs/manual/](https://www.gnu.org/software/emacs/manual/)
* Go Programming Language: [https://go.dev/](https://go.dev/)
* `gofmt` documentation: [https://go.dev/blog/gofmt](https://go.dev/blog/gofmt)
* Emacs Lisp Reference Manual: [https://www.gnu.org/software/emacs/manual/elisp.html](https://www.gnu.org/software/emacs/manual/elisp.html)
* `window-start` (Emacs Lisp function): [https://www.gnu.org/software/emacs/manual/html_node/elisp/Window-Scrolling.html](https://www.gnu.org/software/emacs/manual/html_node/elisp/Window-Scrolling.html)
* `set-window-start` (Emacs Lisp function): [https://www.gnu.org/software/emacs/manual/html_node/elisp/Window-Scrolling.html](https://www.gnu.org/software/emacs/manual/html_node/elisp/Window-Scrolling.html)
* `current-window-configuration` (Emacs Lisp function): [https://www.gnu.org/software/emacs/manual/html_node/elisp/Window-Configurations.html](https://www.gnu.org/software/emacs/manual/html_node/elisp/Window-Configurations.html)
* `set-window-configuration` (Emacs Lisp function): [https://www.gnu.org/software/emacs/manual/html_node/elisp/Window-Configurations.html](https://www.gnu.org/software/emacs/manual/html_node/elisp/Window-Configurations.html)
* `shell-command-on-region` (Emacs Lisp function): [https://www.gnu.org/software/emacs/manual/html_node/elisp/Running-Shell-Commands.html](https://www.gnu.org/software/emacs/manual/html_node/elisp/Running-Shell-Commands.html)
* `erase-buffer` (Emacs Lisp function): [https://www.gnu.org/software/emacs/manual/html_node/elisp/Buffer-Contents.html](https://www.gnu.org/software/emacs/manual/html_node/elisp/Buffer-Contents.html)
* `insert-buffer-substring` (Emacs Lisp function): [https://www.gnu.org/software/emacs/manual/html_node/elisp/Buffer-Contents.html](https://www.gnu.org/software/emacs/manual/html_node/elisp/Buffer-Contents.html)