diff --git a/extensions/typescript-language-features/src/features/completions.ts b/extensions/typescript-language-features/src/features/completions.ts index 4a4a1d9eed9..52e82adf2a8 100644 --- a/extensions/typescript-language-features/src/features/completions.ts +++ b/extensions/typescript-language-features/src/features/completions.ts @@ -77,26 +77,8 @@ class MyCompletionItem extends vscode.CompletionItem { }; } - if (tsEntry.insertText) { - this.insertText = tsEntry.insertText; - - // Set filterText for intelliCode and bracket accessors , but not for `this.` completions since it results in - // them being overly prioritized. #74164 - this.filterText = !(/^this\./).test(tsEntry.insertText) ? tsEntry.insertText : undefined; - - // Handle the case: - // - // ``` - // const xyz = { 'ab c': 1 }; - // xyz.ab| - // ``` - // - // In which case we want to insert a bracket accessor but should use `.abc` as the filter text instead of - // the bracketed insert text. - if (tsEntry.insertText[0] === '[') { - this.filterText = tsEntry.insertText.replace(/^\[['"](.+)[['"]\]$/, '.$1'); - } - } + this.insertText = tsEntry.insertText; + this.filterText = this.getFilterText(line, tsEntry.insertText); if (completionContext.isMemberCompletion && completionContext.dotAccessorContext) { this.filterText = completionContext.dotAccessorContext.text + (this.insertText || this.label); @@ -139,6 +121,42 @@ class MyCompletionItem extends vscode.CompletionItem { this.resolveRange(line); } + private getFilterText(line: string, insertText: string | undefined): string | undefined { + // Handle private field completions + if (this.tsEntry.name.startsWith('#')) { + const wordRange = this.document.getWordRangeAtPosition(this.position); + const wordStart = wordRange ? line.charAt(wordRange.start.character) : undefined; + if (insertText) { + if (insertText.startsWith('this.#')) { + return wordStart === '#' ? insertText : insertText.replace(/^this\.#/, ''); + } else { + return insertText; + } + } else { + return wordStart === '#' ? undefined : this.tsEntry.name.replace(/^#/, ''); + } + return undefined; + } + + // For `this.` completions, generally don't set the filter text since we don't want them to be overly prioritized. #74164 + if (insertText?.startsWith('this.')) { + return undefined; + } + // Handle the case: + // ``` + // const xyz = { 'ab c': 1 }; + // xyz.ab| + // ``` + // In which case we want to insert a bracket accessor but should use `.abc` as the filter text instead of + // the bracketed insert text. + else if (insertText?.startsWith('[')) { + return insertText.replace(/^\[['"](.+)[['"]\]$/, '.$1'); + } + + // In all other cases, fallback to using the insertText + return insertText; + } + private resolveRange(line: string): void { if (this.range) { return; diff --git a/extensions/typescript-language-features/src/features/languageConfiguration.ts b/extensions/typescript-language-features/src/features/languageConfiguration.ts index 59fefa1007b..91cb4a36c91 100644 --- a/extensions/typescript-language-features/src/features/languageConfiguration.ts +++ b/extensions/typescript-language-features/src/features/languageConfiguration.ts @@ -17,7 +17,7 @@ const jsTsLanguageConfiguration: vscode.LanguageConfiguration = { decreaseIndentPattern: /^((?!.*?\/\*).*\*\/)?\s*[\}\]].*$/, increaseIndentPattern: /^((?!\/\/).)*(\{[^}"'`]*|\([^)"'`]*|\[[^\]"'`]*)$/ }, - wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g, + wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g, onEnterRules: [ { // e.g. /** | */ diff --git a/extensions/typescript-language-features/src/test/completions.test.ts b/extensions/typescript-language-features/src/test/completions.test.ts index 46c021d4489..196b034402f 100644 --- a/extensions/typescript-language-features/src/test/completions.test.ts +++ b/extensions/typescript-language-features/src/test/completions.test.ts @@ -27,20 +27,22 @@ async function updateConfig(newConfig: VsCodeConfiguration): Promise { const configDefaults: VsCodeConfiguration = Object.freeze({ - [Config.suggestSelection]: 'first', - [Config.completeFunctionCalls]: false, [Config.autoClosingBrackets]: 'always', + [Config.completeFunctionCalls]: false, [Config.insertMode]: 'insert', + [Config.snippetSuggestions]: 'none', + [Config.suggestSelection]: 'first', }); const _disposables: vscode.Disposable[] = []; @@ -463,6 +465,110 @@ suite('TypeScript Completions', () => { `abc["xy w"]` )); }); + + test('Private field completions on `this.#` should work', async () => { + await enumerateConfig(Config.insertMode, insertModes, async config => { + await createTestEditor(testDocumentUri, + `class A {`, + ` #xyz = 1;`, + ` foo() {`, + ` this.#$0`, + ` }`, + `}`, + ); + + const document = await acceptFirstSuggestion(testDocumentUri, _disposables); + assert.strictEqual( + document.getText(), + joinLines( + `class A {`, + ` #xyz = 1;`, + ` foo() {`, + ` this.#xyz`, + ` }`, + `}`, + ), + `Config: ${config}`); + }); + }); + + test('Private field completions on `#` should insert `this.`', async () => { + await enumerateConfig(Config.insertMode, insertModes, async config => { + await createTestEditor(testDocumentUri, + `class A {`, + ` #xyz = 1;`, + ` foo() {`, + ` #$0`, + ` }`, + `}`, + ); + + const document = await acceptFirstSuggestion(testDocumentUri, _disposables); + assert.strictEqual( + document.getText(), + joinLines( + `class A {`, + ` #xyz = 1;`, + ` foo() {`, + ` this.#xyz`, + ` }`, + `}`, + ), + `Config: ${config}`); + }); + }); + + test('Private field completions should not require strict prefix match (#89556)', async () => { + await enumerateConfig(Config.insertMode, insertModes, async config => { + await createTestEditor(testDocumentUri, + `class A {`, + ` #xyz = 1;`, + ` foo() {`, + ` this.xyz$0`, + ` }`, + `}`, + ); + + const document = await acceptFirstSuggestion(testDocumentUri, _disposables); + assert.strictEqual( + document.getText(), + joinLines( + `class A {`, + ` #xyz = 1;`, + ` foo() {`, + ` this.#xyz`, + ` }`, + `}`, + ), + `Config: ${config}`); + }); + }); + + test('Private field completions without `this.` should not require strict prefix match (#89556)', async () => { + await enumerateConfig(Config.insertMode, insertModes, async config => { + await createTestEditor(testDocumentUri, + `class A {`, + ` #xyz = 1;`, + ` foo() {`, + ` xyz$0`, + ` }`, + `}`, + ); + + const document = await acceptFirstSuggestion(testDocumentUri, _disposables); + assert.strictEqual( + document.getText(), + joinLines( + `class A {`, + ` #xyz = 1;`, + ` foo() {`, + ` this.#xyz`, + ` }`, + `}`, + ), + `Config: ${config}`); + }); + }); }); async function enumerateConfig(configKey: string, values: readonly string[], f: (message: string) => Promise): Promise {