Skip to content

fix(autocomplete): compare whole completions when removing duplicates - #6004

Open
boogie wants to merge 1 commit into
ajaxorg:masterfrom
boogie:fix/completion-duplicate-removal
Open

fix(autocomplete): compare whole completions when removing duplicates#6004
boogie wants to merge 1 commit into
ajaxorg:masterfrom
boogie:fix/completion-duplicate-removal

Conversation

@boogie

@boogie boogie commented Aug 14, 2026

Copy link
Copy Markdown

Problem

FilteredList.setFilter removes duplicates by comparing each completion with the one immediately before it. That is only correct while equal completions stay adjacent, and they do not: the list is sorted by score first, so a duplicate scoring differently from its twin ends up separated by another completion and both reach the popup.

Two completers offering the same word with different scores is enough:

[{value: "foo", score: 5}, {value: "bar", score: 3}, {value: "foo", score: 1}]
// popup shows: foo, bar, foo

The comparison is too coarse in the other direction as well. The key is snippet || caption || value, so snippets are compared by their body alone, and two snippets expanding to the same text collapse into one however differently they are captioned. Ace's own html mode ships fourteen doc snippets that expand to only eight distinct bodies — three of them are dropped from the popup today, and the survivor of each pair is whichever sorts first alphabetically.

Change

Compare what a completion reads as in the popup together with what it inserts, and track every completion kept rather than only the previous one.

before after
same value, separated by a different score both shown merged
same value, adjacent merged merged
distinct captions, same snippet body merged both shown
html mode, doc prefix 11 of 14 shown 14 of 14 shown

Completions that look the same in the popup and insert the same text are still merged, which is what keeps the keyword and text completers from offering the same word twice. Of a set of duplicates the first one after sorting survives, which is the highest scored — the same one that survived before.

The Set replaces a single variable in a function that already sorts the same array immediately above, so the added cost is not measurable against what setFilter does per keystroke.

Test

test: duplicate completions are removed regardless of their position covers both directions at once, and asserts the surviving duplicate is the higher scored one. Against the current code it fails with:

'["foo","bar","foo","snippet a"]' == '["foo","bar","snippet a","snippet b"]'

foo twice, and snippet b missing.

Open kitchen-sink @ 2a773af408f8dfa2c8f61df011b845662b2c4e02

Duplicates are dropped by comparing each completion with the one before it,
which only holds while equal completions stay adjacent. They are sorted by
score first, so a duplicate scoring differently from its twin ends up
separated by another completion and both reach the popup. Two completers
offering the same word with different scores is enough to see it.

The comparison is also too coarse in the other direction. It reads
`snippet || caption || value`, so snippets are compared by their body alone
and two snippets expanding to the same text collapse into one however
differently they are captioned. The html mode ships fourteen `doc` snippets
that expand to only eight distinct bodies, and three of them are dropped from
the popup today.

Compare what the completion reads as together with what it inserts, and track
every completion kept rather than only the previous one. Completions that look
the same in the popup and insert the same text are still merged, and of a set
of duplicates the first after sorting survives, which is the highest scored.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant