IJPL-252099 measure the accepted range against the frontend item pattern

Accepting a completion could leave the leading typed characters in the
document (`de` + `default=` -> `dedefault=`). While a fresh request is in
flight the frontend keeps the popup populated from an older request's
items, re-matched against the prefix typed since (seedStaleAndSwap), and
swaps them out via a restart once the fresh results land. Accepting
before that restart commits an item whose backend matcher is shorter than
the document prefix, and the race cannot be closed -- the user may accept
at any instant -- so the accept itself has to be correct.

The replaced range comes from the mirror arranger's itemPattern, which
LookupArranger composes as registeredMatcherPrefix + myAdditionalPrefix.
On the mirror those halves sync from different sources: the matcher from
the request that produced the item, the additional prefix refreshed from
the lookup on every arranger swap (refreshUi -> checkReused ->
prefixChanged). Composing them is wrong in both directions -- too short
while the matcher lags, and too long once the matcher is extended to the
full prefix, which then swallows a space of the indent.

ItemSelected therefore carries the frontend lookup's own pattern, the one
string the frontend actually measured against. It is optional so a
client/host version skew degrades to the previous behaviour. The mirror
pins it for the committed item and returns it verbatim from itemPattern
rather than rebuilding it; the matcher is still re-registered so case
correction agrees with the pattern, but it no longer influences the
range. An item that does not match the reported pattern is not inserted
at all -- a missing completion is recoverable, a mangled one is not.

The added distributed test has not been seen green: locally the lookup is
torn down before its assertions by unrelated focus and completion-phase
events, so it needs a CI run.

(cherry picked from commit 5a9e3e26e59487027f28284dd8e3eecb2d890c55)

GitOrigin-RevId: fdb36d633cfca8d7de708f8f16824110ea9c1672
This commit is contained in:
Max Medvedev
2026-08-17 18:54:15 +00:00
committed by intellij-monorepo-bot
parent cee3a88548
commit 3f3348b08a
@@ -49,17 +49,28 @@ sealed interface RpcLookupElementEvent {
* older request when this event is handled (IJPL-252099 — the frontend swaps a stale-seeded request for a fresh one
* right before the accept). Resolving against the arranger's own session instead would fail to find the item and
* insert one carrying a shorter prefix matcher, leaving the leading typed characters in the document.
*
* [itemPattern] is the prefix the *frontend* lookup matched the chosen element against
* (`Lookup.itemPattern`), and it is the authoritative measure of the range the insertion must replace: the frontend
* owns the lookup, so it owns the lookup start offset. It can be **longer** than the matcher the backend stored for
* the item — a stale-seeded placeholder shows an older request's items re-matched against the prefix typed since
* (`FrontendCompletionRequestSessionImpl.seedStaleAndSwap`), and accepting one before the swap-restart lands would
* otherwise measure the replaced range against the older, shorter matcher and duplicate the leading typed characters
* (IJPL-252099). `null` means "no pattern reported" (nothing chosen, or an older frontend), in which case the backend
* keeps using the item's own matcher.
*/
@Serializable
data class ItemSelected(
val projectId: ProjectId,
val requestId: RpcCompletionRequestId,
val selectedItemId: RpcCompletionItemId? = null,
val itemPattern: String? = null,
) : RpcLookupElementEvent {
override fun toString(): String = buildToString("ItemSelected") {
field("projectId", projectId)
field("requestId", requestId)
fieldWithNullDefault("selectedItemId", selectedItemId)
fieldWithNullDefault("itemPattern", itemPattern)
}
}