2023-08-18

before-stringに別のオーバーレイのfaceが適用されない

before-stringプロパティを持つオーバーレイ(ov1)があったとします。そのオーバーレイ(ov1)を囲むように別のオーバーレイ(ov2)もあったとします。その別のオーバーレイ(ov2)がfaceプロパティを持っていた場合、そのfaceはov1のbefore-stringに影響するでしょうか。

[OV1BEFORE][OV1TEXT] OV2TEXT][OV2TEXT ov1の範囲ov2の範囲ov1のbefore-stringによって生成された部分青くハイライトするfaceが設定されている
図1: 二つのオーバーレイが重なる様子

色々試してみたのですが、なかなか影響させる方法が見つかりませんでした。

ov1のbefore-stringはov1が囲んでいるテキストの最初の文字のfaceテキストプロパティのみに影響を受けるようです(もちろんbefore-string自体にテキストプロパティが付いている場合は別です)。

これはtransient-mark-modeやhl-line-modeを使ってbefore-stringを持つオーバーレイを囲ってみればよく分かります。before-stringの部分だけハイライトされません。

Emacs Lispで再現するコードは次のようになります。

(let ((beg (point))
      (_ (insert "[OV2TEXT [OV1TEXT] OV2TEXT]"))
      (end (point)))
  (let ((ov1 (make-overlay (+ beg 9) (- end 9))))
    (overlay-put ov1 'evaporate t)
    (overlay-put ov1 'before-string "[OV1BEFORE]"))
  (let ((ov2 (make-overlay beg end)))
    (overlay-put ov2 'evaporate t)
    (overlay-put ov2 'face '(:background "#4080c0"))))

結果は次のようになります。

before-stringに他のオーバーレイのfaceプロパティが適用されない様子
図2: before-stringに他のオーバーレイのfaceプロパティが適用されない様子

色々変えて試してみました。

  • priorityプロパティを色々指定してみても変わりません。
  • ov2の範囲を色々変えても変わりません。
  • after-stringもbefore-stringと同様に影響を受けません。
  • [OV1TEXT]の先頭文字([)にfaceテキストプロパティ(実際にはfont-lock-face)を付けると、before-stringにはそのfaceが適用されます。つまり (put-text-property (+ beg 9) (+ beg 10) 'font-lock-face '(:background "red")) のように。これは回避策には利用できそうですがov2のfaceが適用されているわけではありません。ちなみにafter-stringは[OV1TEXT]の最後の文字……ではなく、その次の文字に設定したfaceが適用されます。
  • displayプロパティで表示した文字列([OV1TEXT]を置き換える)は影響を受けます。

displayプロパティは影響を受ける。その事実を知ったとき、私にはあるアイデアが浮かびました。before-stringにdisplayテキストプロパティを付けたらどうなるんだろう。つまり次のようにするわけです。

(let ((beg (point))
      (_ (insert "[OV2TEXT [OV1TEXT] OV2TEXT]"))
      (end (point)))
  (let ((ov1 (make-overlay (+ beg 9) (- end 9))))
    (overlay-put ov1 'evaporate t)
    (overlay-put ov1 'before-string
                 ;; ↓★displayテキストプロパティを設定する。
                 (propertize "_" 'display "[OV1BEFORE-DISPLAY]")))
  (let ((ov2 (make-overlay beg end)))
    (overlay-put ov2 'evaporate t)
    (overlay-put ov2 'face '(:background "#4080c0"))))

結果は何と……

before-stringに他のオーバーレイのfaceプロパティが適用されている様子
図3: before-stringに他のオーバーレイのfaceプロパティが適用されている様子

ちゃんと適用されました!

これらは一体どう解釈すれば良いのでしょうか。

まずbefore-stringに他のオーバーレイのfaceが適用されないのはバグでしょうか、意図した仕様でしょうか、それとも単に未定義動作(どうなっても文句は言えない)なだけでしょうか。前述したとおりtransient-mark-modeで範囲選択すればすぐに分かるので誰も気が付かないと言うことは無いと思うんですよね。さりとてこの挙動に何かメリットがあるのかと問われればあまり思いつきません。

一方before-stringのdisplayには効くというのはどうなのでしょうか。この挙動に頼って良いものなのでしょうか。

GNUのサイトに行ってバグトラッカーとメールのアーカイブを何度か行ったり来たりした後、嫌になって探すのを諦めました。

個人的にはどちらにも適用されるのが自然な挙動のように感じます。

今回の問題が気になったきっかけは、dired-details-rでhl-line-modeが正しく機能しなかったことです。dired-details-rでは、行末の"\n"部分にオーバーレイをかけてbefore-stringでファイルの詳細情報を表示しています1。なので、ファイルの詳細情報の部分は一切ハイライトされません。これでは現在の行をハイライトする意味がありません。

dired-details-rでhl-line-modeが正しく機能しない様子
図4: dired-details-rでhl-line-modeが正しく機能しない様子

すでに色々回避策を適用してしまったのですが、もしbefore-stringのdisplayに頼って良いのならもっとシンプルで安定したコードに出来そうです。

いやはや、Emacsのテキスト&オーバーレイプロパティまわりは何とも複雑ですね。

(追記:dired-details-rでhl-line-modeが正しく機能しない件は解決しました! ちなみにこのテクニックはall-the-icons-dired(私は色々独自に手を入れて使っています)でも有効です。あれはafter-stringでアイコンを挿入するので、そのままではhl-line-modeでアイコン部分がハイライトされません)

hl-line-modeが完全に機能するようになった様子
図5: hl-line-modeが完全に機能するようになった様子

脚注:

1

その辺りの経緯については以前に書いたと思います。たぶんEmacsでdisplayプロパティを使って改行を置き換えると非常に遅くなる件のあたり