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

[インデックス 19561] ファイルの概要

このコミットは、Go言語のEmacsメジャーモードであるgo-mode.elに対する変更です。go-mode.elは、EmacsエディタでGo言語のコードを編集する際に、シンタックスハイライト、インデント、補完などの機能を提供するLispスクリプトです。このファイルは、EmacsにおけるGo開発の利便性を高めるための重要なコンポーネントです。

コミット

このコミットは、go-mode.el内のgo--delete-whole-line関数の実装を修正し、以前のハック的な実装をより堅牢な独自の実装に置き換えるものです。これにより、Emacsのfletマクロの非推奨化への対応と、kill-ring(キルリング)の破損問題の解決が図られています。

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/2005bea7fd09e62fa1790dd86b04cfd712a0d21a

元コミット内容

misc/emacs: replace hacky go--delete-whole-line with own implementation

Using flet to replace kill-region with delete-region was a hack,
flet is now (GNU Emacs 24.3) deprecated and at least two people
have reported an issue where using go--delete-whole-line would
permanently break their kill ring. While that issue is probably
caused by faulty third party code (possibly prelude), it's easier
to write a clean implementation than to tweak the hack.

LGTM=ruiu, adonovan
R=adonovan, ruiu
CC=adg, golang-codereviews
https://golang.org/cl/106010043

変更の背景

この変更には主に二つの背景があります。

  1. fletマクロの非推奨化: Emacs Lispのfletマクロは、Emacs 24.3以降で非推奨となりました。fletは、関数を一時的に再定義するために使用されるマクロですが、その動的スコープの挙動が問題となる場合がありました。非推奨化された機能を使用し続けることは、将来的な互換性の問題や予期せぬ挙動を引き起こす可能性があるため、代替手段への移行が推奨されます。元のgo--delete-whole-line関数は、fletを使用してkill-regionkill-newというEmacsの組み込み関数を一時的にdelete-regionと空の関数に置き換えることで、キルリングに内容を保存せずに「行を削除する」という動作を実現していました。
  2. kill-ring破損問題: 少なくとも2人のユーザーから、go--delete-whole-lineを使用するとEmacsのkill-ringが恒久的に破損するという報告がありました。kill-ringは、Emacsにおけるクリップボードのようなもので、削除(kill)されたテキストが一時的に保存され、後で貼り付け(yank)できるようになります。この問題は、コミットメッセージによると「おそらくサードパーティのコード(おそらくPrelude)に起因する」とされていますが、go-mode.el側の実装が原因で問題が顕在化していた可能性があります。キルリングの破損は、ユーザーの編集体験に深刻な影響を与えるため、早急な対応が必要でした。

これらの問題に対処するため、既存のハック的な実装を修正するよりも、ゼロからクリーンな実装を書き直す方が容易かつ堅牢であると判断され、今回のコミットに至りました。

前提知識の解説

このコミットの変更内容を理解するためには、以下のEmacs Lispの概念と関数に関する知識が必要です。

  • Emacs Lisp: Emacsエディタの拡張言語であり、Emacsのほぼ全ての機能がEmacs Lispで実装されています。ユーザーはEmacs Lispを使ってEmacsをカスタマイズしたり、新しい機能を追加したりできます。
  • kill-ring (キルリング): Emacsにおける削除(kill)操作で切り取られたテキストが保存される履歴バッファです。C-y (yank) コマンドでキルリングの最新の内容を貼り付けることができます。kill操作はテキストをキルリングに保存しますが、delete操作は保存しません。
  • kill-whole-line: 現在の行全体を削除し、その内容をキルリングに保存するEmacsの組み込み関数です。
  • delete-region: 指定された領域(開始位置と終了位置)のテキストを削除しますが、その内容をキルリングには保存しないEmacsの組み込み関数です。
  • flet: Emacs Lispのマクロで、関数を一時的に再定義するために使用されました。しかし、その動的スコープの挙動が複雑で、Emacs 24.3以降で非推奨となりました。
  • defun: Emacs Lispで新しい関数を定義するためのマクロです。
  • defalias: Emacs Lispで既存の関数に別名を定義するためのマクロです。
  • fboundp: 引数で与えられたシンボルが関数として定義されているかどうかをチェックする関数です。
  • eobp (End Of Buffer Predicate): ポイント(カーソル位置)がバッファの末尾にある場合にt(真)を返す関数です。
  • bobp (Beginning Of Buffer Predicate): ポイントがバッファの先頭にある場合にt(真)を返す関数です。
  • point: 現在のカーソル位置を返す関数です。
  • setq: 変数に値を代入する特殊形式です。
  • or: 論理OR演算子です。
  • cond: 条件分岐を行う特殊形式です。
  • progn: 複数の式を順に評価し、最後の式の値を返す特殊形式です。
  • forward-visible-line: この関数はEmacsの標準関数ではありませんが、名前から推測すると、表示されている行(視覚的な行)に基づいてカーソルを前方に移動させるカスタム関数であると考えられます。Emacsでは、長い行が折り返されて複数行に表示される場合があり、その際に「視覚的な行」という概念が重要になります。
  • end-of-visible-line: 同様に、この関数もEmacsの標準関数ではありませんが、表示されている行の末尾にカーソルを移動させるカスタム関数であると考えられます。
  • save-excursion: この特殊形式は、その内部で実行されるコードがポイント(カーソル位置)や現在のバッファを変更しても、ブロックの実行後に元のポイントとバッファの状態を自動的に復元します。これにより、Lispコードがユーザーの編集状態に影響を与えることなく操作を実行できます。
  • signal: Emacs Lispでエラーを発生させる関数です。signalはエラーシンボルとエラーに関する追加情報を含むリストを引数に取り、エラー処理を開始します。

技術的詳細

以前のgo--delete-whole-lineの実装は、fletを使用してkill-regionkill-newというEmacsの組み込み関数を一時的に再定義することで、キルリングに内容を保存せずに「行を削除する」という目的を達成していました。

(defun go--delete-whole-line (&optional arg)
  ;; Emacs uses both kill-region and kill-new, Xemacs only uses
  ;; kill-region. In both cases we turn them into operations that do
  ;; not modify the kill ring. This solution does depend on the
  ;; implementation of kill-line, but it's the only viable solution
  ;; that does not require to write kill-line from scratch.
  (flet ((kill-region (beg end)
                      (delete-region beg end))
         (kill-new (s) ()))\n    (go--kill-whole-line arg)))

このコードでは、fletを使ってkill-regionが呼ばれたら代わりにdelete-regionを実行し、kill-newが呼ばれたら何もせず(空のリストを返す)という一時的な再定義を行っていました。これにより、go--kill-whole-line(これはkill-whole-lineまたはkill-entire-lineのエイリアス)が内部でkill-regionkill-newを呼び出しても、キルリングに影響を与えないようにしていました。

しかし、このアプローチには以下の問題がありました。

  1. fletの非推奨化: 前述の通り、fletは非推奨となり、将来的な互換性が保証されなくなりました。
  2. kill-ring破損問題: fletによる関数の動的な再定義は、特にサードパーティのパッケージ(例: Prelude)がキルリングの操作をフックしている場合などに、予期せぬ副作用を引き起こす可能性がありました。コミットメッセージにあるように、キルリングが恒久的に破損するという深刻な問題が発生していました。これは、fletが提供する動的スコープが、他のコードの期待する挙動と衝突した結果であると考えられます。

新しい実装では、kill-whole-lineの内部実装に依存するのではなく、行の削除ロジックをgo--delete-whole-line関数内で直接、かつクリーンに再実装しています。これにより、fletの使用を避け、キルリングへの影響を完全に制御できるようになります。

新しいgo--delete-whole-line関数は、kill-whole-lineの動作を模倣しつつ、delete-regionを使用してテキストを削除することで、キルリングに内容を保存しないという要件を満たしています。引数argは、削除する行数を指定します。

  • argが正の場合:現在の行からarg行を前方に削除します。
  • argが負の場合:現在の行からarg行を後方に削除します。
  • argがゼロの場合:現在の行のみを削除します。

また、バッファの先頭や末尾での削除操作に対する境界条件のチェックも含まれており、signal関数を使って適切なエラー(end-of-bufferbeginning-of-buffer)を発生させることで、堅牢性を高めています。

コアとなるコードの変更箇所

misc/emacs/go-mode.el ファイルにおいて、主にgo--delete-whole-line関数の定義が変更されました。

変更前:

;; - Use go--kill-whole-line instead of kill-whole-line (called
;;   kill-entire-line in XEmacs)
;;
;; - Use go--position-bytes instead of position-bytes
(defmacro go--xemacs-p ())
  `(featurep 'xemacs))

(defalias 'go--kill-whole-line
  (if (fboundp 'kill-whole-line)
      #'kill-whole-line
    #'kill-entire-line))

;; Delete the current line without putting it in the kill-ring.
(defun go--delete-whole-line (&optional arg)
  ;; Emacs uses both kill-region and kill-new, Xemacs only uses
  ;; kill-region. In both cases we turn them into operations that do
  ;; not modify the kill ring. This solution does depend on the
  ;; implementation of kill-line, but it's the only viable solution
  ;; that does not require to write kill-line from scratch.
  (flet ((kill-region (beg end)
                      (delete-region beg end))
         (kill-new (s) ()))\n    (go--kill-whole-line arg)))

変更後:

;; - Use go--position-bytes instead of position-bytes
(defmacro go--xemacs-p ()
  `(featurep 'xemacs))\n
;; Delete the current line without putting it in the kill-ring.
(defun go--delete-whole-line (&optional arg)
  ;; Derived from `kill-whole-line'.
  ;; ARG is defined as for that function.
  (setq arg (or arg 1))
  (if (and (> arg 0)
           (eobp)
           (save-excursion (forward-visible-line 0) (eobp)))
      (signal 'end-of-buffer nil))
  (if (and (< arg 0)
           (bobp)
           (save-excursion (end-of-visible-line) (bobp)))
      (signal 'beginning-of-buffer nil))
  (cond ((zerop arg)
         (delete-region (progn (forward-visible-line 0) (point))
                        (progn (end-of-visible-line) (point))))
        ((< arg 0)
         (delete-region (progn (end-of-visible-line) (point))
                        (progn (forward-visible-line (1+ arg))
                               (unless (bobp)
                                 (backward-char))\n                               (point))))
        (t
         (delete-region (progn (forward-visible-line 0) (point))
                        (progn (forward-visible-line arg) (point))))))

主な変更点は以下の通りです。

  • go--kill-whole-linedefalias定義が削除されました。これは、新しい実装がkill-whole-lineに依存しないためです。
  • go--delete-whole-line関数の内部実装が完全に書き直されました。fletの使用が廃止され、行の削除ロジックが直接記述されています。

コアとなるコードの解説

新しいgo--delete-whole-line関数の詳細な解説です。

(defun go--delete-whole-line (&optional arg)
  ;; Derived from `kill-whole-line'.
  ;; ARG is defined as for that function.
  (setq arg (or arg 1))
  ;; argが指定されていない場合、デフォルトで1(現在の行を削除)に設定されます。

  (if (and (> arg 0)
           (eobp)
           (save-excursion (forward-visible-line 0) (eobp)))
      (signal 'end-of-buffer nil))
  ;; argが正(前方に削除)で、かつポイントがバッファの末尾にあり、
  ;; さらに現在の可視行の先頭に移動してもバッファの末尾である場合(つまり、バッファの末尾にいる状態で前方に削除しようとした場合)、
  ;; 'end-of-buffer'エラーを発生させます。
  ;; save-excursionは、forward-visible-line 0の実行後もポイントを元の位置に戻します。

  (if (and (< arg 0)
           (bobp)
           (save-excursion (end-of-visible-line) (bobp)))
      (signal 'beginning-of-buffer nil))
  ;; argが負(後方に削除)で、かつポイントがバッファの先頭にあり、
  ;; さらに現在の可視行の末尾に移動してもバッファの先頭である場合(つまり、バッファの先頭にいる状態で後方に削除しようとした場合)、
  ;; 'beginning-of-buffer'エラーを発生させます。

  (cond ((zerop arg)
         ;; argが0の場合(現在の行のみを削除)
         (delete-region (progn (forward-visible-line 0) (point))
                        ;; 現在の可視行の先頭に移動し、その位置を削除開始点とします。
                        (progn (end-of-visible-line) (point))))
         ;; 現在の可視行の末尾に移動し、その位置を削除終了点とします。
         ;; そして、その範囲をキルリングに保存せずに削除します。

        ((< arg 0)
         ;; argが負の場合(後方に削除)
         (delete-region (progn (end-of-visible-line) (point))
                        ;; 現在の可視行の末尾に移動し、その位置を削除開始点とします。
                        (progn (forward-visible-line (1+ arg))
                               ;; (1+ arg)は負の数なので、後方に移動します。
                               ;; 例えばargが-1なら、(1 + -1) = 0となり、現在の行の先頭に移動します。
                               ;; argが-2なら、(1 + -2) = -1となり、1行前の行の先頭に移動します。
                               (unless (bobp)
                                 (backward-char))
                               ;; バッファの先頭でない限り、1文字後退します。
                               ;; これは、forward-visible-lineが移動先の行の先頭にポイントを置くため、
                               ;; 削除範囲の終端を正確に指定するためです。
                               (point))))
         ;; 指定された行数分後方に移動した位置を削除終了点とします。

        (t
         ;; argが正の場合(前方に削除)
         (delete-region (progn (forward-visible-line 0) (point))
                        ;; 現在の可視行の先頭に移動し、その位置を削除開始点とします。
                        (progn (forward-visible-line arg) (point))))))
         ;; 指定された行数分前方に移動した位置を削除終了点とします。

この新しい実装は、fletのような一時的な関数再定義のハックに頼らず、Emacs Lispの基本的な移動・削除関数を組み合わせて、より直接的かつ堅牢に行削除の機能を実現しています。これにより、fletの非推奨化への対応と、キルリング破損問題の根本的な解決が図られています。

関連リンク

参考にした情報源リンク