[インデックス 11055] ファイルの概要
このコミットは、EmacsエディタのGo言語モード(go-mode.el)におけるシンタックスハイライトの不具合、特にバッククォートで囲まれた文字列(raw string literals)のハイライトに関する修正を目的としています。従来のsyntax-tableを用いた方法から、より柔軟なfont-lockコールバックと拡張されたgo-mode-cs(コメント/文字列の状態管理)メカニズムへの移行により、この問題が解決されています。
コミット
go-mode.el: fix syntax highlighting of backticks
Instead of syntax-tables, an extended go-mode-cs is used for
from a font-lock callback.
Cache invalidation must happen in a before-change-function
because font-lock runs in an after-change-function, potentially
before the cache invalidation takes place.
Performance is reasonable, even with src/pkg/html/entity.go
and test/fixedbugs/bug257.go.
Fixes #2330.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5529045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/70ed0ac5889000fb712dac16e9dea8ef2fa4030f
元コミット内容
commit 70ed0ac5889000fb712dac16e9dea8ef2fa4030f
Author: Florian Weimer <fw@deneb.enyo.de>
Date: Mon Jan 9 12:58:29 2012 -0500
go-mode.el: fix syntax highlighting of backticks
Instead of syntax-tables, an extended go-mode-cs is used for
from a font-lock callback.
Cache invalidation must happen in a before-change-function
because font-lock runs in an after-change-function, potentially
before the cache invalidation takes place.
Performance is reasonable, even with src/pkg/html/entity.go
and test/fixedbugs/bug257.go.
Fixes #2330.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5529045
変更の背景
このコミットの主な背景は、EmacsのGoモード(go-mode.el)におけるバッククォート( )で囲まれたGo言語のraw string literalsのシンタックスハイライトが正しく機能していなかったことです。Go言語では、バッククォートで囲まれた文字列はエスケープシーケンスが解釈されない「生(raw)」の文字列として扱われます。従来のgo-mode.elでは、Emacsのsyntax-tableを用いて文字列やコメントの構文解析を行っていましたが、この方法ではバッククォート文字列のような複雑なケースに柔軟に対応しきれていませんでした。
特に、font-lock(Emacsのシンタックスハイライトシステム)がテキストの変更後に実行されるafter-change-functionとして動作するのに対し、構文解析の状態キャッシュの無効化が適切に行われていなかったため、ハイライトが古い情報に基づいて行われる可能性がありました。このコミットは、これらの問題を解決し、より正確で堅牢なシンタックスハイライトを実現するために導入されました。コミットメッセージにあるFixes #2330は、この問題がGoプロジェクトのIssue 2330で報告されていたことを示しています。
前提知識の解説
このコミットを理解するためには、以下のEmacs LispおよびGo言語に関する知識が必要です。
- Emacs Lisp (Elisp): Emacsエディタの拡張言語であり、Emacsの動作のほとんどはElispで記述されています。
go-mode.elもElispで書かれたファイルです。 - Emacsのシンタックスハイライト (Font Lock mode): Emacsの主要なシンタックスハイライトシステムです。テキストの構文要素(キーワード、文字列、コメントなど)を認識し、それらに対応するフェイス(色、フォントスタイルなど)を適用します。
font-lock-modeは、バッファの内容が変更されると自動的に再ハイライトを行います。 syntax-table: Emacsがテキストの構文を解析するために使用するテーブルです。各文字がどのような構文的役割を持つか(例:単語の区切り、文字列の開始/終了、コメントの開始/終了など)を定義します。syntax-tableは比較的単純な構文解析に適していますが、複雑なネストやコンテキスト依存の構文には限界があります。font-lock-keywords:font-lock-modeがハイライトを行う際に使用する正規表現とフェイスのリストです。これには、キーワード、組み込み関数、定数などのパターンが含まれます。before-change-functionとafter-change-function: Emacsのフック(hook)メカニズムの一部です。before-change-function: バッファの内容が変更される「前」に実行される関数を登録します。after-change-function: バッファの内容が変更された「後」に実行される関数を登録します。- このコミットでは、キャッシュの無効化を
before-change-functionに移動することで、font-lockが古いキャッシュに基づいてハイライトを行うことを防いでいます。
parse-partial-sexp: Emacs Lispの関数で、S式(Symbolic Expression)の構文解析を部分的に行い、現在のポイントの構文状態(文字列内か、コメント内か、括弧のネストレベルなど)を返します。これは、より複雑な構文解析や、テキストプロパティを用いた状態管理に利用されます。- テキストプロパティ (Text Properties): Emacsのテキストに付加できる任意の属性です。特定のテキスト範囲に情報を関連付けることができます。このコミットでは、
go-mode-csというテキストプロパティを使用して、コメントや文字列の範囲をマークし、キャッシュしています。 - Go言語のraw string literals (バッククォート文字列): Go言語では、バッククォート(
)で囲まれた文字列は、エスケープシーケンスが解釈されない「生」の文字列として扱われます。複数行にわたる文字列や、正規表現、HTML/XMLなどのコードを記述する際に便利です。
技術的詳細
このコミットの技術的な核心は、go-mode.elにおけるコメントと文字列の構文解析およびハイライトの方法を根本的に変更した点にあります。
-
syntax-tableからの脱却:- 従来の
go-mode-syntax-tableでは、バッククォート()、シングルクォート(')、ダブルクォート(")を文字列の区切り文字として直接定義していました。 - 変更後、これらの文字の
syntax-tableエントリは.(シンタックス的に特別な意味を持たない文字)に変更されました。これは、syntax-tableによる単純な文字列認識ではなく、より高度なロジックで文字列を検出することを示唆しています。 - コメントの
/や*も同様に.に変更され、コメントもsyntax-tableではなく、別の方法で処理されるようになりました。
- 従来の
-
font-lockコールバックの導入:go-mode-font-lock-keywordsに、go-mode-font-lock-cs-commentとgo-mode-font-lock-cs-stringという新しい関数が追加されました。これらはfont-lockがハイライトを行う際に呼び出すコールバック関数です。- これらのコールバックは、
go-mode-csという新しい関数を利用して、コメントや文字列の範囲を動的に特定し、適切なフェイス(font-lock-comment-faceやfont-lock-string-face)を適用します。
-
拡張された
go-mode-cs関数とテキストプロパティによる状態管理:go-mode-csは、与えられたポイント(カーソル位置)がコメントまたは文字列の内部にあるかどうかを判断し、その範囲を返す関数です。- この関数は、
go-mode-mark-csという内部関数を呼び出し、バッファの指定された範囲内のコメントや文字列を解析し、その範囲にgo-mode-csというテキストプロパティを付加します。このプロパティは、コメント/文字列の開始位置と終了位置のペアを保持します。 - これにより、
font-lockはsyntax-tableに頼ることなく、go-mode-csプロパティを参照してコメントや文字列の範囲を正確に認識できるようになります。特にバッククォート文字列のように、内部に改行や他の特殊文字を含む可能性のある複雑な構造に対応できるようになりました。 go-mode-mark-csの実装は、looking-atやsearch-forwardといった正規表現ベースの検索関数を駆使して、Go言語のコメント(//、/* ... */)と文字列("、'、)の開始と終了を正確に検出します。
-
キャッシュ無効化のタイミングの修正:
- 従来の
go-mode-mark-clear-cacheはafter-change-functionsフックに登録されていました。これは、バッファの内容が変更された「後」にキャッシュをクリアすることを意味します。 - しかし、
font-lockもafter-change-functionとして動作するため、font-lockが古いキャッシュに基づいてハイライトを適用してしまう可能性がありました。 - このコミットでは、
go-mode-mark-clear-cacheをbefore-change-functionsフックに移動しました。これにより、バッファの内容が変更される「前」にキャッシュがクリアされるため、font-lockは常に最新の構文状態に基づいてハイライトを行うことができます。これは、競合状態を回避し、ハイライトの正確性を保証するために非常に重要な変更です。
- 従来の
これらの変更により、go-mode.elはGo言語の複雑な文字列リテラル、特にバッククォート文字列のシンタックスハイライトをより正確かつ効率的に行えるようになりました。
コアとなるコードの変更箇所
misc/emacs/go-mode.el ファイルにおける主要な変更箇所は以下の通りです。
-
go-mode-syntax-tableの変更:--- a/misc/emacs/go-mode.el +++ b/misc/emacs/go-mode.el @@ -44,17 +44,11 @@ (modify-syntax-entry ?< "." st) (modify-syntax-entry ?> "." st) - ;; Strings - (modify-syntax-entry ?\" "\"" st) - (modify-syntax-entry ?\' "\"" st) - (modify-syntax-entry ?` "\"" st) - (modify-syntax-entry ?\\ "\\" st) - - ;; Comments - (modify-syntax-entry ?/ ". 124b" st) - (modify-syntax-entry ?* ". 23" st) - (modify-syntax-entry ?\\n "> b" st) - (modify-syntax-entry ?^m "> b" st) + ;; Strings and comments are font-locked separately. + (modify-syntax-entry ?\" "." st) + (modify-syntax-entry ?\' "." st) + (modify-syntax-entry ?` "." st) + (modify-syntax-entry ?\\ "." st) st) "Syntax table for Go mode.")文字列とコメントの区切り文字の
syntax-tableエントリが、.(特別な意味を持たない文字)に変更されました。 -
go-mode-font-lock-keywordsへのコールバック追加:--- a/misc/emacs/go-mode.el +++ b/misc/emacs/go-mode.el @@ -74,7 +68,9 @@ some syntax analysis.") (constants '("nil" "true" "false" "iota")) (type-name "\\s *\\(?:[*(]\\s *\\)*\\(?:\\w+\\s *\\.\\s *\\)?\\(\\w+\\)") ) - `((,(regexp-opt go-mode-keywords 'words) . font-lock-keyword-face) + `((go-mode-font-lock-cs-comment 0 font-lock-comment-face t) + (go-mode-font-lock-cs-string 0 font-lock-string-face t) (,(regexp-opt go-mode-keywords 'words) . font-lock-keyword-face) (,(regexp-opt builtins 'words) . font-lock-builtin-face) (,(regexp-opt constants 'words) . font-lock-constant-face)go-mode-font-lock-cs-commentとgo-mode-font-lock-cs-stringがfont-lock-keywordsに追加され、コメントと文字列のハイライトを専用の関数で行うようになりました。 -
go-mode-mark-clear-cacheの変更とフックの移動:--- a/misc/emacs/go-mode.el +++ b/misc/emacs/go-mode.el @@ -165,27 +161,25 @@ 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-cs-state nil - "The `parse-partial-sexp' state of the comment/string parser as -of the point `go-mode-mark-cs-end'.") -(make-variable-buffer-local 'go-mode-mark-cs-state) - (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.") (make-variable-buffer-local 'go-mode-mark-nesting-end) -(defun go-mode-mark-clear-cache (b e l)\n- "An after-change-function that clears the comment/string and\n+(defun go-mode-mark-clear-cache (b e)\n+ "A before-change-function that clears the comment/string and\n nesting caches from the modified point on."\n\n (save-restriction\n (widen)\n - (when (< b go-mode-mark-cs-end)\n - (remove-text-properties b (min go-mode-mark-cs-end (point-max)) '(go-mode-cs nil))\n - (setq go-mode-mark-cs-end b\n - go-mode-mark-cs-state nil))\n -\n + (when (<= b go-mode-mark-cs-end)\n + ;; Remove the property adjacent to the change position.\n + ;; It may contain positions pointing beyond the new end mark.\n + (let ((b (let ((cs (get-text-property (max 1 (1- b)) 'go-mode-cs)))\n +\t\t (if cs (car cs) b))))\n +\t(remove-text-properties\n +\t b (min go-mode-mark-cs-end (point-max)) '(go-mode-cs nil))\n +\t(setq go-mode-mark-cs-end b)))\n (when (< b go-mode-mark-nesting-end)\n (remove-text-properties b (min go-mode-mark-nesting-end (point-max)) '(go-mode-nesting nil))\n (setq go-mode-mark-nesting-end b)))) @@ -470,9 +530,8 @@ functions, and some types. It also provides indentation that is \n ;; Reset the syntax mark caches\n (setq go-mode-mark-cs-end 1\n - go-mode-mark-cs-state nil\n go-mode-mark-nesting-end 1)\n -(add-hook 'after-change-functions #'go-mode-mark-clear-cache nil t)\n +(add-hook 'before-change-functions #'go-mode-mark-clear-cache nil t)\n \n ;; Indentation\n (set (make-local-variable 'indent-line-function)\n ``` `go-mode-mark-cs-state`変数が削除され、`go-mode-mark-clear-cache`関数が`before-change-functions`フックに移動されました。キャッシュクリアのロジックも変更され、より正確にテキストプロパティを削除するようになりました。 -
go-mode-csとgo-mode-mark-cs関数の大幅な変更:--- a/misc/emacs/go-mode.el +++ b/misc/emacs/go-mode.el @@ -210,7 +204,7 @@ context-sensitive.") (progn ,@body) (set-buffer-modified-p ,modified-var)))))))\n\n-(defsubst go-mode-cs (&optional pos)\n+(defun go-mode-cs (&optional pos)\n "Return the comment/string state at point POS. If point is\n inside a comment or string (including the delimiters), this\n returns a pair (START . END) indicating the extents of the\n@@ -218,45 +212,111 @@ comment or string."\n\n (unless pos\n (setq pos (point)))\n- (if (= pos 1)\n- nil\n- (when (> pos go-mode-mark-cs-end)\n- (go-mode-mark-cs pos))\n- (get-text-property (- pos 1) 'go-mode-cs)))\n+ (when (> pos go-mode-mark-cs-end)\n+ (go-mode-mark-cs pos))\n+ (get-text-property pos 'go-mode-cs))\n\n (defun go-mode-mark-cs (end)\n "Mark comments and strings up to point END. Don't call this\n directly; use `go-mode-cs'."\n-\n (setq end (min end (point-max)))\n (go-mode-parser\n- (let* ((pos go-mode-mark-cs-end)\n- (state (or go-mode-mark-cs-state (syntax-ppss pos))))\n- ;; Mark comments and strings\n- (when (nth 8 state)\n- ;; Get to the beginning of the comment/string\n- (setq pos (nth 8 state)\n- state nil))\n- (while (> end pos)\n- ;; Find beginning of comment/string\n- (while (and (> end pos)\n- (progn\n- (setq state (parse-partial-sexp pos end nil nil state 'syntax-table)\n- pos (point))\n- (not (nth 8 state)))))\n- ;; Find end of comment/string\n- (let ((start (nth 8 state)))\n- (when start\n- (setq state (parse-partial-sexp pos (point-max) nil nil state 'syntax-table)\n- pos (point))\n- ;; Mark comment\n- (put-text-property start (- pos 1) 'go-mode-cs (cons start pos))\n- (when nil\n- (put-text-property start (- pos 1) 'face\n- `((:background "midnight blue")))))))\n- ;; Update state\n- (setq go-mode-mark-cs-end pos\n- go-mode-mark-cs-state state))))\n+ (save-match-data\n+ (let ((pos\n+\t ;; Back up to the last known state.\n+\t (let ((last-cs\n+\t\t (and (> go-mode-mark-cs-end 1)\n+\t\t\t(get-text-property (1- go-mode-mark-cs-end) \n+\t\t\t\t\t 'go-mode-cs))))\n+\t (if last-cs\n+\t\t (car last-cs)\n+\t\t(max 1 (1- go-mode-mark-cs-end))))))\n+ (while (< pos end)\n+\t (goto-char pos)\n+\t (let ((cs-end\t\t\t; end of the text property\n+\t\t(cond\n+\t\t ((looking-at "//")\n+\t\t (end-of-line)\n+\t\t (point))\n+\t\t ((looking-at "/\\*")\n+\t\t (goto-char (+ pos 2))\n+\t\t (if (search-forward "*/" (1+ end) t)\n+\t\t (point)\n+\t\t end))\n+\t\t ((looking-at "\"")\n+\t\t (goto-char (1+ pos))\n+\t\t (if (looking-at "[^\"\\n\\\\]*\\(\\\\\\\\.[^\"\\n\\\\]*\\)*\"")\n+\t\t (match-end 0)\n+\t\t (end-of-line)\n+\t\t (point)))\n+\t\t ((looking-at "'")\n+\t\t (goto-char (1+ pos))\n+\t\t (if (looking-at "[^'\\n\\\\]*\\(\\\\\\\\.[^'\\n\\\\]*\\)*'")\n+\t\t (match-end 0)\n+\t\t (end-of-line)\n+\t\t (point)))\n+\t\t ((looking-at "`")\n+\t\t (goto-char (1+ pos))\n+\t\t (while (if (search-forward "`" end t)\n+\t\t\t (if (eq (char-after) ?`)\n+\t\t\t\t (goto-char (1+ (point))))\n+\t\t\t (goto-char end)\n+\t\t\t nil))\n+\t\t (point)))))\n+\t (cond\n+\t (cs-end\n+\t (put-text-property pos cs-end 'go-mode-cs (cons pos cs-end))\n+\t (setq pos cs-end))\n+\t ((re-search-forward "[\\\"'`]\\\\|/[/*]" end t)\n+\t (setq pos (match-beginning 0)))\n+\t (t\n+\t (setq pos end)))))\n+ (setq go-mode-mark-cs-end pos)))))\n+\n+\n+\n+(defun go-mode-font-lock-cs (limit comment)\n+ "Helper function for highlighting comment/strings. If COMMENT is t,\n+set match data to the next comment after point, and advance point\n+after it. If COMMENT is nil, use the next string. Returns nil\n+if no further tokens of the type exist."\n+ ;; Ensures that `next-single-property-change' below will work properly.\n+ (go-mode-cs limit)\n+ (let (cs next (result 'scan))\n+ (while (eq result 'scan)\n+ (if (or (>= (point) limit) (eobp))\n+\t (setq result nil)\n+\t(setq cs (go-mode-cs))\n+\t(if cs\n+\t (if (eq (= (char-after (car cs)) ?/) comment)\n+\t\t;; If inside the expected comment/string, highlight it.\n+\t\t(progn\n+\t\t ;; If the match includes a "\\n", we have a\n+\t\t ;; multi-line construct. Mark it as such.\n+\t\t (goto-char (car cs))\n+\t\t (when (search-forward "\\n" (cdr cs) t)\n+\t\t (put-text-property\n+\t\t (car cs) (cdr cs) 'font-lock-multline t))\n+\t\t (set-match-data (list (car cs) (cdr cs) (current-buffer)))\n+\t\t (goto-char (cdr cs))\n+\t\t (setq result t))\n+\t ;; Wrong type. Look for next comment/string after this one.\n+\t (goto-char (cdr cs)))\n+\t ;; Not inside comment/string. Search for next comment/string.\n+\t (setq next (next-single-property-change\n+\t\t (point) 'go-mode-cs nil limit))\n+\t (if (and next (< next limit))\n+\t (goto-char next)\n+\t (setq result nil)))))\n+ result))\n+\n+(defun go-mode-font-lock-cs-string (limit)\n+ "Font-lock iterator for strings."\n+ (go-mode-font-lock-cs limit nil))\n+\n+(defun go-mode-font-lock-cs-comment (limit)\n+ "Font-lock iterator for comments."\n+ (go-mode-font-lock-cs limit t))\n ``` `go-mode-cs`関数は、`parse-partial-sexp`に依存する代わりに、正規表現と`goto-char`, `looking-at`, `search-forward`などの関数を直接使用して、コメントと文字列の範囲を特定するようになりました。また、`go-mode-font-lock-cs`、`go-mode-font-lock-cs-string`、`go-mode-font-lock-cs-comment`といった新しいヘルパー関数が追加され、`font-lock`との連携を強化しています。
コアとなるコードの解説
このコミットの核となる変更は、go-mode.elがコメントと文字列の構文解析をどのように行うかという点にあります。
-
go-mode-syntax-tableの簡素化: 以前は、syntax-tableが文字列やコメントの開始/終了文字を直接定義していました。しかし、syntax-tableは単純な構文解析には適していますが、Go言語のバッククォート文字列のように、内部に改行を含む可能性のある複雑な構造や、ネストされたコメントなどには対応しきれませんでした。このコミットでは、これらの文字のsyntax-tableエントリを一般的な文字(.)に変更することで、syntax-tableによる制約から解放され、より柔軟な解析ロジックを導入する余地が生まれました。 -
go-mode-csの再実装とテキストプロパティの活用:go-mode-cs関数は、与えられた位置がコメントまたは文字列の内部にあるかどうかを判断し、その範囲を返す役割を担います。- この関数の内部では、
go-mode-mark-csが呼び出されます。go-mode-mark-csは、バッファの内容を走査し、Go言語のコメント(//、/* ... */)と文字列("、'、)の開始と終了を正規表現ベースで検出します。 - 検出されたコメントや文字列の範囲には、
go-mode-csという名前のテキストプロパティが付加されます。このプロパティの値は、コメント/文字列の開始位置と終了位置を示すペア((START . END))です。 - これにより、
go-mode-cs関数は、syntax-tableやparse-partial-sexpのような汎用的な構文解析器に頼ることなく、Go言語の構文に特化した方法でコメントや文字列の範囲を正確に特定できるようになりました。特に、バッククォート文字列の複数行にわたる特性も、このカスタム解析ロジックによって適切に処理されます。
-
font-lockコールバックとキャッシュ無効化の連携:go-mode-font-lock-cs-commentとgo-mode-font-lock-cs-stringは、font-lockがハイライトを行う際に呼び出す専用のコールバック関数です。これらの関数は、go-mode-csを利用してコメントや文字列の範囲を取得し、適切なハイライトを適用します。- 最も重要な変更点の一つは、キャッシュ無効化関数
go-mode-mark-clear-cacheがafter-change-functionsからbefore-change-functionsに移動されたことです。 font-lockは通常、バッファの変更後に実行されるため、もしキャッシュクリアがafter-change-functionsで行われると、font-lockが古いキャッシュデータに基づいてハイライトを行ってしまう「競合状態」が発生する可能性がありました。before-change-functionsに移動することで、バッファが変更される前にキャッシュが確実にクリアされるため、font-lockは常に最新の構文状態に基づいてハイライトを行うことが保証されます。これにより、ハイライトの正確性と信頼性が大幅に向上しました。
これらの変更により、go-mode.elはGo言語の構文、特にバッククォート文字列のハイライトにおいて、より堅牢で正確な動作を実現しています。
関連リンク
- Go言語のIssue #2330: https://github.com/golang/go/issues/2330
- Go言語のコードレビューシステム (Gerrit) の変更リスト: https://golang.org/cl/5529045
参考にした情報源リンク
- Emacs Lisp Reference Manual:
- Syntax Tables: https://www.gnu.org/software/emacs/manual/html_node/elisp/Syntax-Tables.html
- Font Lock Mode: https://www.gnu.org/software/emacs/manual/html_node/elisp/Font-Lock-Mode.html
- Change Hooks: https://www.gnu.org/software/emacs/manual/html_node/elisp/Change-Hooks.html
- Text Properties: https://www.gnu.org/software/emacs/manual/html_node/elisp/Text-Properties.html
parse-partial-sexp: https://www.gnu.org/software/emacs/manual/html_node/elisp/Parsing-Expressions.html
- Go言語の仕様 - String literals: https://go.dev/ref/spec#String_literals
- Emacs Lispの
looking-at,search-forward,re-search-forwardなどの正規表現関連関数に関する情報。 - Emacsの
font-lockの仕組みに関する一般的な解説記事。