TypeCellOS / TypeCellOS/BlockNote

openSuggestionMenu: menu closes on the second typed character (single-transaction dispatch breaks queryStartPos)

オープン 初心者向け
#2,949 コメント 1 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

主要言語
TypeScript
スター
10.2k
フォーク
772
平均マージ
3日 11時間
マージ済み PR(30日)
17

説明

Summary

A suggestion menu opened programmatically with editor.openSuggestionMenu(...) closes as soon as a second character is typed into it. The same menu opened by typing the trigger character filters normally. This makes openSuggestionMenu unusable for any programmatic entry point that expects the user to then type a query.

Version: @blocknote/core 0.33.x (React 18 line), reproduced in Chromium.

Impact

We hit this on the side-menu «+» button, which for us is the only way to reach the slash menu on a Russian ЙЦУКЕН keyboard layout — that layout has no / key at all, so «click +, then type» is the whole route. Every slash item is equally affected. It is not layout-specific though: it reproduces with plain Latin keys.

Reproduction

Open the slash menu programmatically and type subp one real keystroke at a time:

editor.setTextCursorPosition(someEmptyBlock);
editor.openSuggestionMenu("/", { deleteTriggerCharacter: true });

Observed, recording whether the menu is open after each keystroke together with the caret's text node:

s : open   "/s"@2
u : CLOSED "/su"@3
b : CLOSED "/sub"@4
p : CLOSED "/subp"@5

Note the document is correct throughout — the trigger character is in the block and the caret is after the query. The menu is simply gone after the second character. Typing /subp by hand in the same editor filters correctly.

deleteTriggerCharacter is not the variable: without it the character is never inserted and it fails the same way; with it the document is right and it still fails.

Cause

openSuggestionMenu does the insert and the plugin meta in one transaction:

// editor/BlockNoteEditor.ts
this.focus();
this.transact((tr) => {
  if (pluginState?.deleteTriggerCharacter) { tr.insertText(triggerCharacter); }
  tr.scrollIntoView().setMeta(this.suggestionMenus.plugins[0], { … });
});

The native path in SuggestionPlugin.handleTextInput dispatches two:

view.dispatch(view.state.tr.insertText(text));
view.dispatch(view.state.tr.setMeta(suggestionMenuPluginKey, { triggerCharacter: snippet }).scrollIntoView());

That matters because the plugin's apply() builds queryStartPos inside the opening transaction's own apply:

const trackedPosition = trackPosition(
  editor,
  newState.selection.from - suggestionPluginTransactionMeta.triggerCharacter.length,
);
…
queryStartPos: () => trackedPosition() + triggerCharacter.length,

and trackPosition slices the editor's accumulated mapping at mapping.maps.length as read at that moment (api/positionMapping.ts). Whether the opening transaction's own map falls inside or outside that slice differs between the one-dispatch and two-dispatch shapes, so queryStartPos() drifts and the next keystroke fails the plugin's own guard:

newState.selection.from < prev.queryStartPos() ||
!newState.selection.$from.sameParent(newState.doc.resolve(prev.queryStartPos()))

The resulting plugin state is otherwise identical field for field between the two paths:

typed / (handleTextInput) openSuggestionMenu("/", { deleteTriggerCharacter: true })
triggerCharacter "/" "/"
deleteTriggerCharacter after apply() meta key absent → undefined !== falsetrue true !== falsetrue
ignoreQueryLength undefined (falsy) false (falsy)
trigger char in the document yes yes
menu survives 2nd keystroke yes no
Confirmation

Replicating handleTextInput's two-transaction shape from application code fixes it completely — multi-character Latin and Cyrillic queries both filter, and the menu matches the typed-/ menu exactly:

const plugin = editor.suggestionMenus.plugins[0];
editor.transact((tr) => tr.insertText("/"));
editor.transact((tr) => tr.setMeta(plugin, { triggerCharacter: "/" }).scrollIntoView());

Forcing those two back into a single transaction reproduces the failure exactly, with everything else unchanged — that is the only variable.

Suggested fix

Split the dispatch inside openSuggestionMenu so the meta is set in a transaction after the insert, matching handleTextInput. Applications otherwise have to reach for editor.suggestionMenus.plugins[0] as a meta key to get a working programmatic entry point, which is what we have had to do.

Happy to open a PR if the approach looks right.

コントリビューションガイド

コントリビューションガイドを開く

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

調査の方向性

editor/BlockNoteEditor.ts の openSuggestionMenu から始め、そのトランザクションを SuggestionPlugin.handleTextInput のトランザクションと比較します。api/positionMapping.ts を読んで queryStartPos のマッピング動作を理解し、次に insert とメタデータの dispatch を分割することで、複数文字のラテン文字およびキリル文字のクエリに対してメニューが開いたままになり、入力された trigger のパスと一致することを確認します。

索引モデルが issue の本文から書いたものです。

評価

技術スタック
react, typescript
領域
frontend
issue の種類
バグ
難易度
2/5
見積もり時間
1〜3時間
活発さ
活発
明瞭さ
明確に書かれている
初心者へのやさしさ
82/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。