[インデックス 12773] ファイルの概要
コミット
このコミット bf9620ebbdf6f9dfa2f46e5823f6afc5bfa8206f
は、Go言語のEmacsメジャーモードである go-mode.el
における、特定の文字で終わる行の誤った解析による過剰なインデントの問題を修正するものです。特に、コメントや文字列の解析ロジックが改善され、インデントの正確性が向上しています。
コミットハッシュ: bf9620ebbdf6f9dfa2f46e5823f6afc5bfa8206f
作者: Ben Fried ben.fried@gmail.com
日付: Mon Mar 26 23:26:39 2012 -0400
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/bf9620ebbdf6f9dfa2f46e5823f6afc5bfa8206f
元コミット内容
misc/emacs: fix overindentation caused by mis-parsing lines ending with special chars
Fixes #3313
go-mode-backward-skip-comments is probably due for a more ambitious refactoring --- it repeats guard conditions after every nearly every movement of point.
R=sameer, r
CC=golang-dev
https://golang.org/cl/5844063
変更の背景
この変更は、Go言語のEmacsモード (go-mode.el
) において、特定の特殊文字(例えば、カンマ ,
や文字列の終わり、コメントの終わりなど)で終わる行が正しく解析されず、結果としてEmacsがコードを過剰にインデントしてしまう問題(Issue #3313)を解決するために行われました。
Emacsのメジャーモードは、プログラミング言語の構文を理解し、適切なインデント、シンタックスハイライト、その他の編集支援機能を提供します。go-mode.el
はGo言語のためにこれらの機能を提供しますが、コメントや文字列の内部にある特殊文字を正しく識別できない場合、インデントエンジンが誤った判断を下し、コードの整形が崩れることがあります。
特に、go-mode-backward-skip-comments
のような関数は、インデント計算のためにコメントや空白をスキップする役割を担いますが、その実装が複雑で、多くのガード条件が重複していることが指摘されており、より大規模なリファクタリングが必要であるという認識がありました。このコミットは、その大規模なリファクタリングの一部ではなく、緊急性の高いインデント問題を修正するためのものです。
前提知識の解説
このコミットを理解するためには、以下の知識が役立ちます。
- Emacs Lisp (Elisp): Emacsエディタの拡張言語であり、Emacsの動作をカスタマイズしたり、新しい機能を追加したりするために使用されます。
go-mode.el
はElispで書かれています。 - Emacsのメジャーモード: 特定のプログラミング言語やファイルタイプに特化した編集機能を提供するEmacsの拡張機能です。シンタックスハイライト、自動インデント、コード補完などが含まれます。
- テキストプロパティ (Text Properties): Emacsのバッファ内のテキストに付加できる属性情報です。シンタックスハイライト、コメント、文字列の範囲などをマークするために使用されます。
go-mode.el
では、go-mode-cs
(コメント/文字列)、go-mode-string
、go-mode-comment
といったカスタムテキストプロパティが定義され、コードの特定の部分がコメントや文字列であるかを識別するために利用されています。 - インデントエンジン: Emacsがコードのインデントレベルを決定するためのロジックです。言語の構文規則に基づいて、現在の行がどれだけ字下げされるべきかを計算します。
- 正規表現 (Regular Expressions): テキストパターンを記述するための強力なツールです。Emacs Lispでは、
looking-at
やre-search-forward
などの関数で正規表現が使用され、コード内の特定の構文要素(例: コメントの開始//
や/*
、文字列の開始"
や'
や`
)を検出します。 - Go言語の構文: Go言語のコメント(
//
と/* */
)や文字列リテラル(ダブルクォート""
、シングルクォート''
、バッククォート``)の構文を理解していると、
go-mode.el` がどのようにこれらを解析しようとしているかがより明確になります。
技術的詳細
このコミットの主要な技術的変更点は、go-mode.el
がコメントと文字列を解析し、それらの範囲をテキストプロパティとしてマークする方法の改善にあります。これにより、インデントエンジンがコードの構造をより正確に理解できるようになります。
-
新しいテキストプロパティの導入:
go-mode-mark-string-end
: 文字列キャッシュの終了位置を管理するための変数。go-mode-mark-comment-end
: コメントキャッシュの終了位置を管理するための変数。 これらの変数は、バッファのどの範囲までコメントや文字列の解析が完了しているかを追跡し、変更があった場合にキャッシュを無効化して再解析を促すために使用されます。
-
コメントと文字列の解析ロジックの分離と強化:
go-mode-in-comment
およびgo-mode-mark-comment
: コメントの検出とマーキングに特化した関数が導入されました。これにより、go-mode-cs
(コメント/文字列) という汎用的なプロパティに依存するのではなく、コメントの範囲をより正確に識別できるようになります。特に、//
スタイルのコメントの終わりを(1+ (point))
で正しく設定するよう修正されています。go-mode-in-string
およびgo-mode-mark-string
: 文字列の検出とマーキングに特化した関数が導入されました。Go言語の3種類の文字列リテラル(ダブルクォート、シングルクォート、バッククォート)をそれぞれ正確に解析し、エスケープシーケンスなども考慮して文字列の終了位置を特定するロジックが追加されています。
-
go-mode-backward-skip-comments
の改善:- この関数は、インデント計算のためにコメントや空白を後方にスキップする役割を担います。以前の実装は複雑で、コメントや空白の検出が不正確な場合がありました。
- 新しい実装では、
go-mode-in-comment
とgo-mode-whitespace-p
(新しいヘルパー関数) を利用して、現在のポイントがコメント内にあるか、または空白であるかをより確実に判断します。 - コメント内にある場合は、コメントの開始位置までジャンプし、空白の場合は空白をスキップします。これにより、インデント計算の際に不要な文字を正確に無視できるようになりました。
-
インデントロジックの調整:
go-mode-indentation
関数において、カンマ,
で終わる行のインデントが修正されました。以前はカンマで終わる行が正しくインデントされないことがありましたが、when (looking-at "\\w+\\s *:.+,\\s *$")
の条件が追加され、このようなケースでインデントレベルが適切に増加するようになりました。go-mode-indent
関数に(let ((case-fold-search nil)))
が追加されました。これは、キーワードと識別子を区別するために、インデント計算中に大文字・小文字を区別しない検索を一時的に無効にするものです。例えば、default
はキーワードですが、Default
は変数名である可能性があり、これらを正しく区別することでインデントの精度が向上します。go-mode-backward-skip-comments
の呼び出し位置が調整され、beginning-of-line
の後にbackward-char
が追加されることで、行の先頭に移動してからコメントをスキップする際に、より正確な位置から処理を開始できるようになりました。looking-back
の正規表現に|package
が追加され、package
キーワードもインデントの考慮対象となりました。
これらの変更により、go-mode.el
はGoコードの構文、特にコメントや文字列の境界をより正確に認識し、その結果としてより信頼性の高い自動インデントを提供できるようになりました。
コアとなるコードの変更箇所
変更はすべて misc/emacs/go-mode.el
ファイル内で行われています。
-
キーバインディングの追加:
@@ -110,6 +110,7 @@ built-ins, functions, and some types.") (let ((m (make-sparse-keymap))) (define-key m "}" #'go-mode-insert-and-indent) (define-key m ")" #'go-mode-insert-and-indent) + (define-key m "," #'go-mode-insert-and-indent) (define-key m ":" #'go-mode-delayed-electric) ;; In case we get : indentation wrong, correct ourselves (define-key m "=" #'go-mode-insert-and-indent)
カンマ
,
がgo-mode-insert-and-indent
にバインドされました。 -
新しいキャッシュ変数の定義:
@@ -161,6 +162,18 @@ will be marked from the beginning up to this point (that is, up to and including character (1- go-mode-mark-cs-end)).") (make-variable-buffer-local 'go-mode-mark-cs-end) +(defvar go-mode-mark-string-end 1 + "The point at which the string cache ends. The buffer +will be marked from the beginning up to this point (that is, up +to and including character (1- go-mode-mark-string-end)).") +(make-variable-buffer-local 'go-mode-mark-string-end) + +(defvar go-mode-mark-comment-end 1 + "The point at which the comment cache ends. The buffer +will be marked from the beginning up to this point (that is, up +to and including character (1- go-mode-mark-comment-end)).") +(make-variable-buffer-local 'go-mode-mark-comment-end) + (defvar go-mode-mark-nesting-end 1 "The point at which the nesting cache ends. The buffer will be marked from the beginning up to this point.")
go-mode-mark-string-end
とgo-mode-mark-comment-end
が追加されました。 -
キャッシュ無効化ロジックの追加:
@@ -180,6 +193,24 @@ nesting caches from the modified point on." (remove-text-properties b (min go-mode-mark-cs-end (point-max)) '(go-mode-cs nil))\ (setq go-mode-mark-cs-end b)))\ +\ + (when (<= b go-mode-mark-string-end) + ;; Remove the property adjacent to the change position. + ;; It may contain positions pointing beyond the new end mark. + (let ((b (let ((cs (get-text-property (max 1 (1- b)) 'go-mode-string)))) +\t\t (if cs (car cs) b)))) +\t(remove-text-properties +\t b (min go-mode-mark-string-end (point-max)) '(go-mode-string nil))\ +\t(setq go-mode-mark-string-end b)))\ + (when (<= b go-mode-mark-comment-end) + ;; Remove the property adjacent to the change position. + ;; It may contain positions pointing beyond the new end mark. + (let ((b (let ((cs (get-text-property (max 1 (1- b)) 'go-mode-comment)))) +\t\t (if cs (car cs) b)))) +\t(remove-text-properties +\t b (min go-mode-mark-string-end (point-max)) '(go-mode-comment nil))\ +\t(setq go-mode-mark-comment-end b)))\ + (when (< b go-mode-mark-nesting-end) (remove-text-properties b (min go-mode-mark-nesting-end (point-max)) '(go-mode-nesting nil))\ (setq go-mode-mark-nesting-end b))))
go-mode-mark-string-end
とgo-mode-mark-comment-end
に基づくキャッシュ無効化ロジックが追加されました。 -
コメント解析の修正:
@@ -237,7 +268,7 @@ directly; use `go-mode-cs'." (cond ((looking-at "//") (end-of-line) - (point))\ + (1+ (point)))\ ((looking-at "/\\*") (goto-char (+ pos 2))\ (if (search-forward "*/" (1+ end) t)\
//
コメントの終了位置の計算が(point)
から(1+ (point))
に変更されました。 -
go-mode-in-comment
とgo-mode-mark-comment
の追加:@@ -273,7 +304,114 @@ directly; use `go-mode-cs'." (setq pos end)))))\ (setq go-mode-mark-cs-end pos)))))\ \ +(defun go-mode-in-comment (&optional pos)\ + "Return the comment/string state at point POS. If point is\ +inside a comment (including the delimiters), this\ +returns a pair (START . END) indicating the extents of the\ +comment or string."\ +\ + (unless pos\ + (setq pos (point)))\ + (when (> pos go-mode-mark-comment-end)\ + (go-mode-mark-comment pos))\ + (get-text-property pos 'go-mode-comment))\ +\ +(defun go-mode-mark-comment (end)\ + "Mark comments up to point END. Don't call this directly; use `go-mode-in-comment'."\ + (setq end (min end (point-max)))\ + (go-mode-parser\ + (save-match-data\ + (let ((pos\ +\t ;; Back up to the last known state.\ +\t (let ((last-comment\ +\t\t (and (> go-mode-mark-comment-end 1)\ +\t\t\t(get-text-property (1- go-mode-mark-comment-end) \ +\t\t\t\t\t 'go-mode-comment))))\ +\t (if last-comment\ +\t\t (car last-comment)\ +\t\t(max 1 (1- go-mode-mark-comment-end))))))\ + (while (< pos end)\ +\t (goto-char pos)\ +\t (let ((comment-end\t\t\t; end of the text property\ +\t\t(cond\ +\t\t ((looking-at "//")\ +\t\t (end-of-line)\ +\t\t (1+ (point)))\ +\t\t ((looking-at "/\\*")\ +\t\t (goto-char (+ pos 2))\ +\t\t (if (search-forward "*/" (1+ end) t)\ +\t\t (point)\ +\t\t end)))))\ +\t (cond\ +\t (comment-end\ +\t (put-text-property pos comment-end 'go-mode-comment (cons pos comment-end))\ +\t (setq pos comment-end))\ +\t ((re-search-forward "/[/*]" end t)\ +\t (setq pos (match-beginning 0)))\ +\t (t\ +\t (setq pos end)))))\ + (setq go-mode-mark-comment-end pos)))))\
コメントの検出とマーキングのための専用関数が追加されました。
-
go-mode-in-string
とgo-mode-mark-string
の追加:+(defun go-mode-in-string (&optional pos)\ + "Return the string state at point POS. If point is\ +inside a string (including the delimiters), this\ +returns a pair (START . END) indicating the extents of the\ +comment or string."\ +\ + (unless pos\ + (setq pos (point)))\ + (when (> pos go-mode-mark-string-end)\ + (go-mode-mark-string pos))\ + (get-text-property pos 'go-mode-string))\ +\ +(defun go-mode-mark-string (end)\ + "Mark strings up to point END. Don't call this\ +directly; use `go-mode-in-string'."\ + (setq end (min end (point-max)))\ + (go-mode-parser\ + (save-match-data\ + (let ((pos\ +\t ;; Back up to the last known state.\ +\t (let ((last-cs\ +\t\t (and (> go-mode-mark-string-end 1)\ +\t\t\t(get-text-property (1- go-mode-mark-string-end) \ +\t\t\t\t\t 'go-mode-string))))\ +\t (if last-cs\ +\t\t (car last-cs)\ +\t\t(max 1 (1- go-mode-mark-string-end))))))\ + (while (< pos end)\ +\t (goto-char pos)\ +\t (let ((cs-end\t\t\t; end of the text property\ +\t\t(cond \ +\t\t ((looking-at "\"")\ +\t\t (goto-char (1+ pos))\ +\t\t (if (looking-at "[^\"\\n\\\\]*\\\\(\\\\\\\\.[^\"\\n\\\\]*\\\\)*\"")\ +\t\t (match-end 0)\ +\t\t (end-of-line)\ +\t\t (point)))\ +\t\t ((looking-at "'")\ +\t\t (goto-char (1+ pos))\ +\t\t (if (looking-at "[^'\\n\\\\]*\\\\(\\\\\\\\.[^'\\n\\\\]*\\\\)*'")\ +\t\t (match-end 0)\ +\t\t (end-of-line)\ +\t\t (point)))\ +\t\t ((looking-at "`")\ +\t\t (goto-char (1+ pos))\ +\t\t (while (if (search-forward "`" end t)\ +\t\t\t (if (eq (char-after) ?`)\ +\t\t\t\t (goto-char (1+ (point))))\ +\t\t\t (goto-char end)\ +\t\t\t nil))\ +\t\t (point)))))\ +\t (cond\ +\t (cs-end\ +\t (put-text-property pos cs-end 'go-mode-string (cons pos cs-end))\ +\t (setq pos cs-end))\ +\t ((re-search-forward "[\"'`]" end t)\ +\t (setq pos (match-beginning 0)))\ +\t (t\ +\t (setq pos end)))))\ + (setq go-mode-mark-string-end pos)))))\
文字列の検出とマーキングのための専用関数が追加されました。
-
go-mode-whitespace-p
の追加:@@ -406,21 +544,31 @@ token on the line." (when (/= (skip-chars-backward "[:word:]_") 0) (not (looking-at go-mode-non-terminating-keywords-regexp)))))))\ \ +(defun go-mode-whitespace-p (char)\ + "Is char whitespace in the syntax table for go."\ + (eq 32 (char-syntax char)))\ +\ (defun go-mode-backward-skip-comments ()\ "Skip backward over comments and whitespace."\ - (when (not (bobp))\ - (backward-char))\ - (while (and (not (bobp))\ - (or (eq 32 (char-syntax (char-after (point))))\ - (go-mode-cs)))\ - (skip-syntax-backward "-")\ - (when (and (not (bobp)) (eq 32 (char-syntax (char-after (point)))))\ - (backward-char))\ - (when (go-mode-cs)\ - (let ((pos (previous-single-property-change (point) 'go-mode-cs)))\ - (if pos (goto-char pos) (goto-char (point-min))))))\ - (when (and (not (go-mode-cs)) (eq 32 (char-syntax (char-after (1+ (point))))))\ - (forward-char 1)))\ + ;; only proceed if point is in a comment or white space\ + (if (or (go-mode-in-comment)\ +\t (go-mode-whitespace-p (char-after (point))))\ + (let ((loop-guard t))\ +\t(while (and\ +\t\tloop-guard\ +\t\t(not (bobp)))\ +\ +\t (cond ((go-mode-whitespace-p (char-after (point)))\ +\t\t ;; moves point back over any whitespace\ +\t\t (re-search-backward "[^[:space:]]"))\ +\ +\t\t((go-mode-in-comment)\ +\t\t ;; move point to char preceeding current comment\ +\t\t (goto-char (1- (car (go-mode-in-comment)))))\ +\t\t\ +\t\t;; not in a comment or whitespace? we must be done.\ +\t\t(t (setq loop-guard nil)\ +\t\t (forward-char 1)))))))
空白文字を判定するヘルパー関数
go-mode-whitespace-p
と、go-mode-backward-skip-comments
の大幅なリファクタリングが行われました。 -
インデントロジックの調整:
@@ -467,10 +615,10 @@ indented one level." (incf indent tab-width))\ ((?\\()\ (goto-char (car nest))\ - (beginning-of-line)\ (go-mode-backward-skip-comments)\ + (backward-char)\ ;; Really just want the token before\ - (when (looking-back "\\<import\\|const\\|var\\|type"\ + (when (looking-back "\\<import\\|const\\|var\\|type\\|package"\ (max (- (point) 7) (point-min)))\ (incf indent tab-width)\ (when first\ @@ -481,9 +629,13 @@ indented one level." (when (looking-at "\\<case\\>\\|\\<default\\>\\|\\w+\\s *:\\(\\S.\\|$\\)")\ (decf indent tab-width))\ \ +\t (when (looking-at "\\w+\\s *:.+,\\s *$")\ +\t (incf indent tab-width))\ +\ ;; Continuation lines are indented 1 level\ - (beginning-of-line)\ - (go-mode-backward-skip-comments)\ + (beginning-of-line)\t\t; back up to end of previous line\ +\t (backward-char)\ + (go-mode-backward-skip-comments) ; back up past any comments\ (when (case (char-before)\ ((nil ?\\{ ?:)\ ;; At the beginning of a block or the statement\ @@ -517,12 +669,15 @@ indented one level." "Indent the current line according to `go-mode-indentation'."\ (interactive)\ \ - (let ((col (go-mode-indentation)))\ - (when col\ - (let ((offset (- (current-column) (current-indentation))))\ - (indent-line-to col)\ - (when (> offset 0)\ - (forward-char offset))))))\ + ;; turn off case folding to distinguish keywords from identifiers\ + ;; e.g. "default" is a keyword; "Default" can be a variable name.\ + (let ((case-fold-search nil))\ + (let ((col (go-mode-indentation)))\ + (when col\ +\t(let ((offset (- (current-column) (current-indentation))))\ +\t (indent-line-to col)\ +\t (when (> offset 0)\ +\t (forward-char offset)))))))
カンマで終わる行のインデント修正、
package
キーワードの考慮、case-fold-search
の一時的な無効化など、インデントロジックが調整されました。
コアとなるコードの解説
このコミットの核心は、Emacs Lispで書かれた go-mode.el
がGo言語のソースコードをより正確に解析し、適切なインデントを提供するための改善です。
-
go-mode-mark-string-end
とgo-mode-mark-comment-end
: これらの変数は、Emacsのテキストプロパティシステムと連携して動作します。Emacsはバッファ内のテキストに任意のプロパティを付加できます。go-mode.el
は、コードのどの部分が文字列で、どの部分がコメントであるかを識別し、その情報をテキストプロパティとして保存します。これらの*-end
変数は、最後に解析された文字列またはコメントの終了位置を記録することで、バッファが変更された際に、変更点より前のキャッシュされた解析結果を再利用し、変更点以降のみを再解析する効率的なメカニズムを提供します。これにより、大規模なファイルでもインデント計算が高速に行われます。 -
go-mode-in-comment
とgo-mode-mark-comment
:go-mode-in-comment
は、現在のカーソル位置がコメント内にあるかどうかを判断します。これは、go-mode-mark-comment
を呼び出して、必要に応じてコメントの範囲を解析し、go-mode-comment
というテキストプロパティとしてマークすることで実現されます。特に、//
スタイルのコメントの終わりを(1+ (point))
とすることで、改行文字を含めてコメントの範囲を正確に捉えるよう修正されています。これは、インデント計算がコメントの直後から始まるべきか、それともコメントの内部にあるべきかを正確に判断するために重要です。 -
go-mode-in-string
とgo-mode-mark-string
:go-mode-in-string
は、現在のカーソル位置が文字列内にあるかどうかを判断します。go-mode-mark-string
は、Go言語の3種類の文字列リテラル(ダブルクォート""
、シングルクォート''
、バッククォート`
)をそれぞれ個別に処理するロジックを含んでいます。特に、エスケープシーケンス(例:\"
)を正しく処理し、文字列の実際の終了位置を特定することが重要です。これにより、文字列の内部にある特殊文字がインデントエンジンによって誤って解釈されることを防ぎます。 -
go-mode-backward-skip-comments
のリファクタリング: この関数は、インデント計算の際に、現在の行の先頭から後方に遡って、意味のあるコードトークンを見つけるためにコメントや空白をスキップします。以前の実装は、コメントと空白の検出が混在しており、複雑でした。新しい実装では、go-mode-in-comment
とgo-mode-whitespace-p
を利用することで、ロジックが明確になり、より堅牢になりました。これにより、インデントの基準となるコード要素を正確に特定できるようになり、結果としてインデントの精度が向上します。 -
インデントロジックの微調整:
- カンマ
,
で終わる行のインデント修正は、Go言語の構造体や関数呼び出しなどでよく見られるパターンに対応するためのものです。例えば、複数行にわたる引数リストや構造体リテラルにおいて、カンマの後に続く行が適切にインデントされるようになります。 case-fold-search nil
の導入は、Emacsの検索機能がデフォルトで大文字・小文字を区別しない場合があるため、キーワード(例:default
)とユーザー定義の識別子(例:DefaultValue
)を正確に区別するために重要です。これにより、インデントエンジンがGo言語の構文要素をより厳密に認識し、誤ったインデントを防ぎます。package
キーワードのインデント考慮は、Go言語のファイルが常にpackage
宣言から始まるため、そのインデントが正しく行われるようにするための基本的な修正です。
- カンマ
これらの変更は、Emacsの go-mode
がGo言語のコードをより「Goらしく」インデントできるようにするための、細部にわたる改善であり、開発者のコーディング体験を向上させるものです。
関連リンク
- Go言語のEmacsモード: https://github.com/golang/go/tree/master/misc/emacs
- Issue 3313:
go-mode.el
overindents lines ending with special chars: https://github.com/golang/go/issues/3313
参考にした情報源リンク
- Emacs Lisp Reference Manual: https://www.gnu.org/software/emacs/manual/elisp.html
- Go Programming Language Specification: https://go.dev/ref/spec
- Git commit
bf9620ebbdf6f9dfa2f46e5823f6afc5bfa8206f
on GitHub: https://github.com/golang/go/commit/bf9620ebbdf6f9dfa2f46e5823f6afc5bfa8206f - Gerrit Code Review for Go: https://go-review.googlesource.com/c/go/+/5844063 (これはコミットメッセージに記載されているCLリンクですが、現在はGitHubにミラーされています)