メンション挿入時にインラインフォーマットが失われるバグを修正
プロンプトメニューからメンションを挿入する際、周囲のボールドなどのインライン書式が失われていたバグを修正しました。テキストノード再構築時に元の書式情報を引き継ぐ #cloneTextNodeFormatting メソッドを導入しています。
背景
プロンプト挿入(prompt insertion) は、ユーザーが入力したトリガーテキストを replaceTextBackUntil で除去し、メンションのアタッチメントノードに置き換える処理です。この処理では、カーソル前後のテキストを一度分割して新しいテキストノードとして再構築しますが、その際に元テキストノードが持っていた書式情報が引き継がれていませんでした。結果として、ボールド体の途中にメンションを挿入すると、挿入前後のテキストが通常体に戻ってしまう問題が発生していました。
技術的な変更
#performTextReplacement の呼び出し前に $getSelection() で現在の選択状態を取得し、新たに追加された #cloneTextNodeFormatting メソッドへ渡すことで書式を引き継ぐようになりました。
変更前:
replaceTextBackUntil(stringToReplace, replacementNodes) {
replacementNodes = Array.isArray(replacementNodes) ? replacementNodes : [ replacementNodes ]
this.editor.update(() => {
const { anchorNode, offset } = this.#getTextAnchorData()
if (!anchorNode) return
const lastIndex = this.#findLastIndexBeforeCursor(anchorNode, offset, stringToReplace)
if (lastIndex === -1) return
this.#performTextReplacement(anchorNode, offset, lastIndex, replacementNodes)
})
}
変更後:
replaceTextBackUntil(stringToReplace, replacementNodes) {
replacementNodes = Array.isArray(replacementNodes) ? replacementNodes : [ replacementNodes ]
const selection = $getSelection()
const { anchorNode, offset } = this.#getTextAnchorData()
if (!anchorNode) return
const lastIndex = this.#findLastIndexBeforeCursor(anchorNode, offset, stringToReplace)
if (lastIndex === -1) return
this.#performTextReplacement(anchorNode, selection, offset, lastIndex, replacementNodes)
}
テキストノードの再生成箇所では、$createTextNode の素の呼び出しから #cloneTextNodeFormatting 経由の生成に切り替わっています。
変更前:
const textNodeBefore = $createTextNode(textBeforeString)
const textNodeAfter = $createTextNode(textAfterCursor || " ")
変更後:
const textNodeBefore = this.#cloneTextNodeFormatting(anchorNode, selection, textBeforeString)
const textNodeAfter = this.#cloneTextNodeFormatting(anchorNode, selection, textAfterCursor || " ")
新設された #cloneTextNodeFormatting の実装は次のとおりです。
#cloneTextNodeFormatting(anchorNode, selection, text) {
const parent = anchorNode.getParent()
const fallbackFormat = parent?.getTextFormat?.() || 0
const fallbackStyle = parent?.getTextStyle?.() || ""
const format = $isRangeSelection(selection) && selection.format
? selection.format
: (anchorNode.getFormat() || fallbackFormat)
const style = $isRangeSelection(selection) && selection.style
? selection.style
: (anchorNode.getStyle() || fallbackStyle)
// ...
}
書式の取得優先順位は「選択状態(RangeSelection)のformat/style → anchorNode自身のformat/style → 親ノードのフォールバック」の3段階です。これにより、選択範囲を伴う挿入でも単純なカーソル位置での挿入でも、いずれのケースで書式が正しく引き継がれます。
また、replaceTextBackUntil から this.editor.update() のラッパーが取り除かれています。これは $getSelection() が Lexical の update コンテキスト外では機能しないためで、呼び出し元がすでに update コンテキスト内で実行されることを前提とした変更です。
設計判断
書式の取得元を複数フォールバックで束ねる設計 が採用されました。
Lexical のテキストノードは format(ビットフラグによるbold/italic等)と style(インラインCSSスタイル)の2つの書式情報を持ちます。RangeSelection が有効な場合はそちらを優先することで、ユーザーが書式を適用した状態でトリガー入力した場合にも対応しています。anchorNode.getFormat() が0(未設定)のときは親ノードから取得するフォールバックにより、ネストされたマークアップ構造でも書式が失われません。
新規メソッドを #performTextReplacement に分離したことで、テキストノード生成ロジックから書式継承ロジックが独立し、将来のスタイル取得方法の変更が局所化されています。
まとめ
本修正は、テキストノードの再構築時に書式情報を明示的に引き継ぐ #cloneTextNodeFormatting を導入することで、プロンプト挿入のあらゆる呼び出し経路における書式消失を根本的に解決しています。クリック選択とキーボード選択の両方を網羅するシステムテストも追加されており、同種のリグレッションを防ぐ安全網が整備されました。