fix(editor): List identical completion items once

kotlin-language-server repeats a class once per classpath entry declaring
it, so `String()` and `String` were listed three times each.
This commit is contained in:
l0ng-ai committed 2026-10-28 08:58:41 +08:00
1 parent b9f8a0b4bd
commit 7c09a13afc
3 files changed
+44 -5

No files matched your search

+15 -1
View File
@@ -4,7 +4,14 @@ import { EditorView } from "@codemirror/view";
import type { CompletionItem } from "@/bindings";
import type { EditorDocContext } from "@/editor/api";
import { lspDoc } from "./lsp";
import { completionSymbolKind, identifierEnd, toCmCompletion, triggerCharacter, typeColumnText } from "./completionItems";
import {
completionSymbolKind,
identifierEnd,
toCmCompletion,
triggerCharacter,
typeColumnText,
withoutDuplicates,
} from "./completionItems";
import {
applyLspCompletion,
callParens,
@@ -75,6 +82,13 @@ describe("toCmCompletion", () => {
expect(toCmCompletion(item({ label: "foo", filterText: "foo_bar" })).label).toBe("foo_bar");
});
it("lists identical items once", () => {
const ctor = item({ label: "String()", kind: "constructor", data: "1" });
const overload = item({ label: "String()", kind: "constructor", labelDetail: "(bytes: ByteArray)" });
const out = withoutDuplicates([ctor, { ...ctor, data: "2" }, overload, { ...ctor, server: 2 }]);
expect(out).toEqual([ctor, overload, { ...ctor, server: 2 }]);
});
it("boosts preselected and demotes deprecated items", () => {
expect(toCmCompletion(item({ preselect: true })).boost).toBe(99);
expect(toCmCompletion(item({ deprecated: true })).boost).toBeLessThan(0);
+13 -4
View File
@@ -35,7 +35,14 @@ import {
type RequestId,
} from "@/platform/ipc";
import { useProject } from "@/platform/project";
import { completionSymbolKind, identifierEnd, toCmCompletion, triggerCharacter, typeColumnText } from "./completionItems";
import {
completionSymbolKind,
identifierEnd,
toCmCompletion,
triggerCharacter,
typeColumnText,
withoutDuplicates,
} from "./completionItems";
import { lspSnippetToCm, snippetVariables } from "./snippet";
import { docOf, isUnavailable, lspDoc, lspRequest, reportLspError } from "./lsp";
import { offsetToPos, posToOffset, textEditsToChanges } from "./positions";
@@ -437,9 +444,11 @@ async function lspCompletionSource(ctx: CompletionContext): Promise<CompletionRe
const req: RequestContext = { doc: doc.doc, path: doc.path, language: doc.uiMode, pos: ctx.pos, text: ctx.state.doc };
const prefs = editorPrefs().completion;
const typed = word?.text ?? "";
let items = prefs.matchCase
? list.items.filter((it) => matchesCase(it.filterText ?? it.label, typed, prefs.caseSensitivity))
: list.items;
let items = withoutDuplicates(
prefs.matchCase
? list.items.filter((it) => matchesCase(it.filterText ?? it.label, typed, prefs.caseSensitivity))
: list.items,
);
// A server snippet named like a live template (jdtls `sout`, `fori`) would be listed twice; the template wins.
if (items.some((it) => it.kind === "snippet")) {
const own = await contributedSource(ctx);
+16
View File
@@ -65,6 +65,22 @@ function extendsLabel(item: CompletionItem): boolean {
return item.filterText!.startsWith(item.label) && /^[^\p{L}\p{N}_$]/u.test(rest);
}
/**
* Items that look and insert the same, listed once. kotlin-language-server
* repeats a class once per classpath entry declaring it (`String()` ×3);
* these all have their own `apply` (see `makeCompletion`), so CodeMirror
* wouldn't merge them.
*/
export function withoutDuplicates(items: readonly CompletionItem[]): CompletionItem[] {
const seen = new Set<string>();
return items.filter((it) => {
const key = JSON.stringify([it.server, it.label, it.labelDetail, it.detail, it.kind, it.insertText, it.additionalEdits]);
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
const IMPORT_SOURCE = [
/^\s*from\s+([\w.]+)\s+import\b/m, // Python
/^\s*import\s+(?:static\s+)?([\w.]+?)\.\w+;?\s*$/m, // Java, Kotlin