[インデックス 15745] ファイルの概要
このコミットは、EmacsエディタのGo言語モード(go-mode.el
)におけるインデントの不具合を修正するものです。具体的には、複数行にわたる関数宣言の後に続く関数本体のインデントが正しく行われない問題を解決します。
コミット
misc/emacs: 複数行関数宣言に続くコードのインデントを修正
複数行にわたって宣言された関数の本体を正しくインデントします。例については http://play.golang.org/p/MHMwNDbFyf を参照してください。
以前は、関数本体は関数宣言の継続行と同じ深さでインデントされていました。現在は、func
キーワードと同じ深さでインデントされるようになります。
R=adonovan, cw, patrick.allen.higgins CC=golang-dev https://golang.org/cl/7628043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/69eb3457279b1067e3c45e735d32916a41b87246
元コミット内容
commit 69eb3457279b1067e3c45e735d32916a41b87246
Author: Dominik Honnef <dominik.honnef@gmail.com>
Date: Tue Mar 12 21:37:18 2013 -0700
misc/emacs: Fix indentation for code following multiline function declarations
Correctly indent the body of functions that have been declared
over multiple lines. See http://play.golang.org/p/MHMwNDbFyf for
an example.
Previously, the body of the function would be indented as deep as
the continuation line of the function declaration. Now it gets
indented as deep as the func keyword.
R=adonovan, cw, patrick.allen.higgins
CC=golang-dev
https://golang.org/cl/7628043
変更の背景
このコミットが行われる前、EmacsのGoモード(go-mode.el
)には、Go言語の関数が複数行にわたって宣言されている場合に、その関数本体のインデントが不適切になるという問題がありました。
Go言語では、関数のシグネチャが長くなる場合、引数や戻り値の型を複数行に分割して記述することがよくあります。例えば、以下のようなコードです。
func someLongFunctionName(
arg1 type1,
arg2 type2,
) (
ret1 typeA,
ret2 typeB,
) {
// ここが関数本体
}
従来のgo-mode.el
では、上記のような複数行の関数宣言において、関数本体の開始を示す{
(波括弧)が、func
キーワードの開始位置ではなく、関数宣言の最後の行(この例では ret2 typeB,
の行)のインデントレベルに合わせてインデントされてしまうという問題がありました。これにより、コードの視認性が損なわれ、Go言語の一般的なコーディングスタイルに反するインデントになっていました。
このコミットは、この不適切なインデントを修正し、関数本体の開始波括弧が常にfunc
キーワードのインデントレベルに揃うようにすることで、より自然で読みやすいGoコードのインデントを実現することを目的としています。
前提知識の解説
Emacs Lisp (Elisp)
Emacs Lispは、Emacsエディタの拡張言語であり、Emacsの動作のほとんどを制御しています。Emacsのカスタマイズや新しい機能の追加は、Emacs Lispで記述されたファイル(通常.el
拡張子を持つ)をロードすることで行われます。go-mode.el
もEmacs Lispで書かれたファイルであり、Go言語のシンタックスハイライト、インデント、その他の編集支援機能を提供します。
Emacsのインデントシステム
Emacsのインデントは、現在の行の適切なインデントレベルを計算し、そのレベルに合わせて空白を挿入することで行われます。これは通常、indent-line-function
という変数に設定された関数によって制御されます。Goモードでは、go-indentation-at-point
のような関数がこの役割を担い、Go言語の構文規則に基づいてインデントを決定します。
インデントの計算には、以下の概念が重要です。
point
: Emacsにおけるカーソルの現在位置。current-indentation
: 現在の行の先頭から最初の非空白文字までの文字数(インデントレベル)。beginning-of-defun
: 現在の関数定義の先頭にpoint
を移動させるEmacs Lisp関数。go-paren-level
: Goコードにおける括弧のネストレベルを計算するgo-mode.el
内の関数。tab-width
: Emacsでタブが何文字分のスペースとして扱われるかを定義する変数。
Go言語の関数宣言
Go言語の関数宣言は、func
キーワードから始まり、関数名、引数リスト、戻り値リスト、そして関数本体のブロック({}
で囲まれた部分)が続きます。
func FunctionName(parameters) (returnValues) {
// Function body
}
引数リストや戻り値リストが長い場合、可読性のために複数行に分割して記述することが推奨されます。このコミットは、この複数行にわたる宣言形式におけるインデントの問題に対処しています。
技術的詳細
このコミットの核心は、go-mode.el
に新しいEmacs Lisp関数を導入し、既存のインデント計算ロジックを修正することによって、複数行の関数宣言における関数本体のインデントを正しく行う点にあります。
主な変更点は以下の通りです。
-
go--at-function-definition
関数の追加: この関数は、現在のpoint
(カーソル位置)が関数定義の開始波括弧({
)上にあるかどうかを判定します。これは、beginning-of-defun
を使って関数定義の先頭に移動し、そこから波括弧を探し、その位置が現在のpoint
と一致するかどうかを確認することで実現されます。これにより、インデントを計算する際に、現在の位置が関数本体の開始点であるかを正確に識別できるようになります。 -
go--indentation-for-opening-parenthesis
関数の追加: この関数は、現在のpoint
が開いている括弧(特に波括弧)上にある場合に、その括弧に対する適切なインデントレベルを返します。 もしgo--at-function-definition
が真を返す(つまり、現在の波括弧が関数定義の開始波括弧である)場合、この関数はbeginning-of-defun
で関数定義の先頭(func
キーワードの位置)に移動し、その行のインデントレベル(current-indentation
)を返します。 そうでない場合は、現在の行のインデントレベルをそのまま返します。これにより、関数本体の開始波括弧のインデントが、func
キーワードのインデントに揃えられるようになります。 -
go-indentation-at-point
関数の修正:go-indentation-at-point
は、EmacsのGoモードにおける主要なインデント計算関数です。この関数が、行の開始波括弧のインデントを決定する際に、新しく追加されたgo--indentation-for-opening-parenthesis
関数を使用するように変更されました。 以前は、単にcurrent-indentation
(現在の行のインデント)を返していましたが、この変更により、関数定義の開始波括弧であればfunc
キーワードのインデントが適用されるようになりました。
これらの変更により、EmacsはGoコードのインデントを計算する際に、関数定義の特殊なケースを認識し、複数行にわたる関数宣言であっても、関数本体のインデントをGoの慣習に従って正しく調整できるようになりました。
コアとなるコードの変更箇所
misc/emacs/go-mode.el
ファイルにおいて、以下の変更が行われました。
追加された関数
go--at-function-definition
go--indentation-for-opening-parenthesis
修正された関数
go-indentation-at-point
--- a/misc/emacs/go-mode.el
+++ b/misc/emacs/go-mode.el
@@ -233,6 +233,28 @@ STOP-AT-STRING is not true, over strings.\"\n (puthash cur-line val go-dangling-cache))))\n val))\n \n+(defun go--at-function-definition ()\n+ \"Return non-nil if point is on the opening curly brace of a\n+function definition.\n+\n+We do this by first calling (beginning-of-defun), which will take\n+us to the start of *some* function. We then look for the opening\n+curly brace of that function and compare its position against the\n+curly brace we are checking. If they match, we return non-nil.\"\n+ (if (= (char-after) ?\\{)\n+ (save-excursion\n+ (let ((old-point (point))\n+ start-nesting)\n+ (beginning-of-defun)\n+ (when (looking-at \"func \")\n+ (setq start-nesting (go-paren-level))\n+ (skip-chars-forward \"^{\")\n+ (while (> (go-paren-level) start-nesting)\n+ (forward-char)\n+ (skip-chars-forward \"^{\") 0)\n+ (if (and (= (go-paren-level) start-nesting) (= old-point (point)))\n+ t))))))\n+\n (defun go-goto-opening-parenthesis (&optional char)\n (let ((start-nesting (go-paren-level)))\n (while (and (not (bobp))\n@@ -245,6 +267,20 @@ STOP-AT-STRING is not true, over strings.\"\n (go-goto-beginning-of-string-or-comment)\n (backward-char))))))\n \n+(defun go--indentation-for-opening-parenthesis ()\n+ \"Return the semantic indentation for the current opening parenthesis.\n+\n+If point is on an opening curly brace and said curly brace\n+belongs to a function declaration, the indentation of the func\n+keyword will be returned. Otherwise the indentation of the\n+current line will be returned.\"\n+ (save-excursion\n+ (if (go--at-function-definition)\n+ (progn\n+ (beginning-of-defun)\n+ (current-indentation))\n+ (current-indentation))))\n+\n (defun go-indentation-at-point ()\n (save-excursion\n (let (start-nesting (outindent 0))\n@@ -258,7 +294,7 @@ STOP-AT-STRING is not true, over strings.\"\n (go-goto-opening-parenthesis (char-after))\n (if (go-previous-line-has-dangling-op-p)\n (- (current-indentation) tab-width)\n- (current-indentation)))\n+ (go--indentation-for-opening-parenthesis)))\n ((progn (go--backward-irrelevant t) (looking-back go-dangling-operators-regexp))\n ;; only one nesting for all dangling operators in one operation\n (if (go-previous-line-has-dangling-op-p)\n@@ -269,7 +305,7 @@ STOP-AT-STRING is not true, over strings.\"\n ((progn (go-goto-opening-parenthesis) (< (go-paren-level) start-nesting))\n (if (go-previous-line-has-dangling-op-p)\n (current-indentation)\n- (+ (current-indentation) tab-width)))\n+ (+ (go--indentation-for-opening-parenthesis) tab-width)))\n (t\n (current-indentation))))))\n \n```
## コアとなるコードの解説
### `go--at-function-definition`
```elisp
(defun go--at-function-definition ()
"Return non-nil if point is on the opening curly brace of a
function definition.
We do this by first calling (beginning-of-defun), which will take
us to the start of *some* function. We then look for the opening
curly brace of that function and compare its position against the
curly brace we are checking. If they match, we return non-nil."
(if (= (char-after) ?\{) ; 現在のカーソル位置が '{' であるかを確認
(save-excursion ; 現在のカーソル位置を保存し、関数終了時に復元
(let ((old-point (point)) ; '{' の元の位置を保存
start-nesting)
(beginning-of-defun) ; 現在の関数定義の先頭に移動
(when (looking-at "func ") ; 'func ' で始まる行であるかを確認
(setq start-nesting (go-paren-level)) ; 現在の括弧のネストレベルを保存
(skip-chars-forward "^{\") ; '{' または '(' をスキップして進む
(while (> (go-paren-level) start-nesting) ; ネストレベルが開始時より大きい間、
(forward-char) ; 1文字進む
(skip-chars-forward "^{\") 0) ; '{' または '(' をスキップ
(if (and (= (go-paren-level) start-nesting) (= old-point (point))) ; ネストレベルが戻り、元の '{' の位置と一致するか確認
t)))))) ; 一致すればt(真)を返す
この関数は、現在のカーソル位置がGo言語の関数定義の開始波括弧({
)上にあるかどうかを判定します。これは、インデントを決定する上で、その波括弧が関数本体の開始を示すものなのか、それとも単なるコードブロックの開始なのかを区別するために重要です。
処理の流れは以下の通りです。
- 現在のカーソル位置の文字が
{
であるかを確認します。 save-excursion
を使って現在のカーソル位置を保存し、処理後に元の位置に戻るようにします。beginning-of-defun
を呼び出し、現在の関数定義の先頭(通常はfunc
キーワードの行)にカーソルを移動させます。looking-at "func "
で、その行がfunc
で始まることを確認し、関数定義であることを特定します。go-paren-level
を使って括弧のネストレベルを追跡し、関数定義の開始波括弧を見つけます。- 見つけた波括弧の位置が、関数呼び出し時に保存しておいた元のカーソル位置と一致すれば、現在のカーソル位置が関数定義の開始波括弧上にあると判断し、
t
(真)を返します。
go--indentation-for-opening-parenthesis
(defun go--indentation-for-opening-parenthesis ()
"Return the semantic indentation for the current opening parenthesis.
If point is on an opening curly brace and said curly brace
belongs to a function declaration, the indentation of the func
keyword will be returned. Otherwise the indentation of the
current line will be returned."
(save-excursion
(if (go--at-function-definition) ; go--at-function-definition が真を返すか(関数定義の開始波括弧か)
(progn ; 真の場合
(beginning-of-defun) ; 関数定義の先頭に移動
(current-indentation)) ; その行のインデントを返す
(current-indentation)))) ; 偽の場合、現在の行のインデントを返す
この関数は、開いている括弧(特に波括弧)に対する「意味的な」インデントレベルを計算して返します。
- もし
go--at-function-definition
がt
を返す(つまり、現在の波括弧が関数定義の開始波括弧である)場合、beginning-of-defun
でfunc
キーワードの行に移動し、その行のインデントレベル(current-indentation
)を返します。これにより、関数本体の開始波括弧がfunc
キーワードと同じインデントレベルに揃えられます。 - そうでない場合(通常のコードブロックの開始など)、現在の行のインデントレベルをそのまま返します。
go-indentation-at-point
の修正
(defun go-indentation-at-point ()
(save-excursion
(let (start-nesting (outindent 0))
;; ... (既存のコード) ...
(cond
((and (= (char-after) ?\{) (go-goto-opening-parenthesis (char-after)))
(if (go-previous-line-has-dangling-op-p)
(- (current-indentation) tab-width)
- (current-indentation))) ; 変更前
+ (go--indentation-for-opening-parenthesis))) ; 変更後
;; ... (既存のコード) ...
((progn (go-goto-opening-parenthesis) (< (go-paren-level) start-nesting))
(if (go-previous-line-has-dangling-op-p)
(current-indentation)
- (+ (current-indentation) tab-width))) ; 変更前
+ (+ (go--indentation-for-opening-parenthesis) tab-width))) ; 変更後
(t
(current-indentation))))))
go-indentation-at-point
は、EmacsのGoモードにおけるインデント計算の主要な関数です。この関数内のcond
式(条件分岐)において、波括弧のインデントを決定する部分が修正されました。
変更前は、単にcurrent-indentation
(現在の行のインデント)を返していましたが、変更後は新しく定義されたgo--indentation-for-opening-parenthesis
関数を呼び出すように変更されました。これにより、関数定義の開始波括弧の場合にのみ、func
キーワードのインデントレベルが適用されるようになり、複数行の関数宣言におけるインデントの不具合が解消されました。
関連リンク
- GitHub上のコミットページ: https://github.com/golang/go/commit/69eb3457279b1067e3c45e735d32916a41b87246
- Go Playgroundの例(コミットメッセージに記載されていたもの):http://play.golang.org/p/MHMwNDbFyf (ただし、このリンクの内容はコミットの修正内容を直接示すものではなく、単に"Hello, 世界"と表示されるようです。元のコミットメッセージが書かれた時点では、問題を示すGoコードの例が掲載されていた可能性があります。)
- Gerrit Code Review (Goプロジェクトのコードレビューシステム): https://golang.org/cl/7628043
参考にした情報源リンク
- 上記のGitHubコミット情報
- Emacs Lispのドキュメント(
defun
,save-excursion
,beginning-of-defun
,current-indentation
,looking-at
,char-after
,point
,cond
,let
,progn
など) - Go言語の関数宣言に関する一般的な知識
go-mode.el
の既存のコードベース(インデント関連の関数や変数)- https://www.gnu.org/software/emacs/manual/html_node/elisp/ (Emacs Lisp Manual)
- https://go.dev/doc/effective_go#functions (Effective Go - Functions)