From b9f8a0b4bdbc4fed5ef4bffd098e578679f0b9dc Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Wed, 28 Oct 2026 08:51:15 +0800 Subject: [PATCH] fix(editor): Rank the exact type first in Java constructor completion jdtls appends the call or body to filterText (`String(${0})`); matching on the label instead lets typing `String` rank `String` above `StringBuilder` and the other types sharing its sort text. --- src/editor/lsp/completion.test.ts | 10 ++++++++++ src/editor/lsp/completion.ts | 4 +++- src/editor/lsp/completionItems.ts | 12 +++++++++++- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/editor/lsp/completion.test.ts b/src/editor/lsp/completion.test.ts index 7a0ce18cc..c56459f30 100644 --- a/src/editor/lsp/completion.test.ts +++ b/src/editor/lsp/completion.test.ts @@ -65,6 +65,16 @@ describe("toCmCompletion", () => { expect(c.displayLabel).toBe("@GetMapping"); }); + it("filters on the label when filterText only adds the call or body", () => { + for (const filterText of ["String(${0})", "List<>()", "Runnable() {\n\t${0}\n};"]) { + const label = filterText.match(/^\w+/)![0]; + const c = toCmCompletion(item({ label, filterText })); + expect(c.label).toBe(label); + expect(c.displayLabel).toBeUndefined(); + } + expect(toCmCompletion(item({ label: "foo", filterText: "foo_bar" })).label).toBe("foo_bar"); + }); + it("boosts preselected and demotes deprecated items", () => { expect(toCmCompletion(item({ preselect: true })).boost).toBe(99); expect(toCmCompletion(item({ deprecated: true })).boost).toBeLessThan(0); diff --git a/src/editor/lsp/completion.ts b/src/editor/lsp/completion.ts index 5452d34d0..bb12f8235 100644 --- a/src/editor/lsp/completion.ts +++ b/src/editor/lsp/completion.ts @@ -363,7 +363,9 @@ export function makeCompletion(item: CompletionItem, req: RequestContext): LspCo // sharing a filterText (`times(other: Int)`, `times(other: Long)`…) and the same // name imported from different modules need their own. apply: - cm.displayLabel || item.additionalEdits.length ? (...a: Parameters) => applyLsp(...a) : applyLsp, + cm.displayLabel || (item.filterText && item.filterText !== item.label) || item.additionalEdits.length + ? (...a: Parameters) => applyLsp(...a) + : applyLsp, info: (c: Completion) => renderInfo(c as LspCompletion), }); } diff --git a/src/editor/lsp/completionItems.ts b/src/editor/lsp/completionItems.ts index 36ce274d7..f4fc42ed2 100644 --- a/src/editor/lsp/completionItems.ts +++ b/src/editor/lsp/completionItems.ts @@ -41,7 +41,7 @@ export function completionSymbolKind(kind: CompletionKind | null): string { * original label is shown through `displayLabel`. */ export function toCmCompletion(item: CompletionItem): Completion { - const filter = item.filterText && item.filterText !== item.label ? item.filterText : null; + const filter = item.filterText && item.filterText !== item.label && !extendsLabel(item) ? item.filterText : null; const c: Completion = { label: filter ?? item.label, type: completionSymbolKind(item.kind), @@ -55,6 +55,16 @@ export function toCmCompletion(item: CompletionItem): Completion { return c; } +/** + * A `filterText` that is the label plus what the item inserts after it (jdtls + * `String(${0})`, `List<>()`): typing a word never reaches that part, and + * matching on it would rank the exact `String` like `StringBuilder`. + */ +function extendsLabel(item: CompletionItem): boolean { + const rest = item.filterText!.slice(item.label.length); + return item.filterText!.startsWith(item.label) && /^[^\p{L}\p{N}_$]/u.test(rest); +} + const IMPORT_SOURCE = [ /^\s*from\s+([\w.]+)\s+import\b/m, // Python /^\s*import\s+(?:static\s+)?([\w.]+?)\.\w+;?\s*$/m, // Java, Kotlin