-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
fix(suggestion): keep dismissed state after dismissal #7570
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@tiptap/suggestion": patch | ||
| --- | ||
|
|
||
| Suggestions dismissed via Escape no longer reappear when the user keeps typing in the same word — they only come back after inserting whitespace, a newline, or moving the cursor to a different trigger. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -6,6 +6,25 @@ import { Decoration, DecorationSet } from '@tiptap/pm/view' | |||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| import { findSuggestionMatch as defaultFindSuggestionMatch } from './findSuggestionMatch.js' | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||
| * Returns true if the transaction inserted any whitespace or newline character. | ||||||||||||||||||||||||||||||||||||||
| * Used to determine when a dismissed suggestion should become active again. | ||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||
| function hasInsertedWhitespace(transaction: Transaction): boolean { | ||||||||||||||||||||||||||||||||||||||
| if (!transaction.docChanged) { | ||||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| return transaction.steps.some(step => { | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+13
to
+17
|
||||||||||||||||||||||||||||||||||||||
| const slice = (step as any).slice | ||||||||||||||||||||||||||||||||||||||
| if (!slice?.content) { | ||||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| // textBetween with '\n' as block separator catches both inline spaces and newlines | ||||||||||||||||||||||||||||||||||||||
| const inserted = slice.content.textBetween(0, slice.content.size, '\n') | ||||||||||||||||||||||||||||||||||||||
| return /\s/.test(inserted) | ||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| export interface SuggestionOptions<I = any, TSelected = any> { | ||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||
| * The plugin key for the suggestion plugin. | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -381,6 +400,9 @@ export function Suggestion<I = any, TSelected = any>({ | |||||||||||||||||||||||||||||||||||||
| text: null | string | ||||||||||||||||||||||||||||||||||||||
| composing: boolean | ||||||||||||||||||||||||||||||||||||||
| decorationId?: string | null | ||||||||||||||||||||||||||||||||||||||
| /** Position of the trigger char when the suggestion was dismissed via Escape. | ||||||||||||||||||||||||||||||||||||||
| * Non-null means "stay dismissed until the user leaves this word or inserts whitespace". */ | ||||||||||||||||||||||||||||||||||||||
| dismissedFrom: number | null | ||||||||||||||||||||||||||||||||||||||
| } = { | ||||||||||||||||||||||||||||||||||||||
| active: false, | ||||||||||||||||||||||||||||||||||||||
| range: { | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -390,6 +412,7 @@ export function Suggestion<I = any, TSelected = any>({ | |||||||||||||||||||||||||||||||||||||
| query: null, | ||||||||||||||||||||||||||||||||||||||
| text: null, | ||||||||||||||||||||||||||||||||||||||
| composing: false, | ||||||||||||||||||||||||||||||||||||||
| dismissedFrom: null, | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| return state | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -414,6 +437,10 @@ export function Suggestion<I = any, TSelected = any>({ | |||||||||||||||||||||||||||||||||||||
| next.range = { from: 0, to: 0 } | ||||||||||||||||||||||||||||||||||||||
| next.query = null | ||||||||||||||||||||||||||||||||||||||
| next.text = null | ||||||||||||||||||||||||||||||||||||||
| // Remember where the dismissed suggestion was so we can suppress re-activation | ||||||||||||||||||||||||||||||||||||||
| // within the same word. If somehow exit fires without an active suggestion, carry | ||||||||||||||||||||||||||||||||||||||
| // the existing dismissedFrom forward so it isn't accidentally cleared. | ||||||||||||||||||||||||||||||||||||||
| next.dismissedFrom = prev.active ? prev.range.from : prev.dismissedFrom | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| return next | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -458,12 +485,31 @@ export function Suggestion<I = any, TSelected = any>({ | |||||||||||||||||||||||||||||||||||||
| transaction, | ||||||||||||||||||||||||||||||||||||||
| })) | ||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||
| next.active = true | ||||||||||||||||||||||||||||||||||||||
| next.decorationId = prev.decorationId ? prev.decorationId : decorationId | ||||||||||||||||||||||||||||||||||||||
| next.range = match.range | ||||||||||||||||||||||||||||||||||||||
| next.query = match.query | ||||||||||||||||||||||||||||||||||||||
| next.text = match.text | ||||||||||||||||||||||||||||||||||||||
| // Resolve dismissed state before activating. | ||||||||||||||||||||||||||||||||||||||
| // Un-dismiss when: the match is at a different trigger position (different word), | ||||||||||||||||||||||||||||||||||||||
| // or the user inserted whitespace / a newline (deliberate continuation of input). | ||||||||||||||||||||||||||||||||||||||
| if (next.dismissedFrom !== null) { | ||||||||||||||||||||||||||||||||||||||
| const sameWord = match.range.from === next.dismissedFrom | ||||||||||||||||||||||||||||||||||||||
| if (!sameWord || hasInsertedWhitespace(transaction)) { | ||||||||||||||||||||||||||||||||||||||
| next.dismissedFrom = null | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+492
to
+495
|
||||||||||||||||||||||||||||||||||||||
| const sameWord = match.range.from === next.dismissedFrom | |
| if (!sameWord || hasInsertedWhitespace(transaction)) { | |
| next.dismissedFrom = null | |
| } | |
| // Keep dismissedFrom in sync with document changes so comparisons to | |
| // match.range.from stay correct even when content is inserted/removed | |
| // before the trigger position (including remote/collab changes). | |
| if (transaction.docChanged) { | |
| const mapped = transaction.mapping.mapResult(next.dismissedFrom) | |
| next.dismissedFrom = mapped.deleted ? null : mapped.pos | |
| } | |
| if (next.dismissedFrom !== null) { | |
| const sameWord = match.range.from === next.dismissedFrom | |
| if (!sameWord || hasInsertedWhitespace(transaction)) { | |
| next.dismissedFrom = null | |
| } | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Import formatting looks off (
exitSuggestion,Suggestionmissing a space after the comma) and doesn’t match the surrounding style; running the formatter should normalize this.