Don't require strict prefix matches for private field completions

Fixxes #89556
This commit is contained in:
Matt Bierner 2020-02-04 17:17:00 -08:00
parent b4a835f5b9
commit 58fe34bb77
3 changed files with 149 additions and 25 deletions

View file

@ -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;

View file

@ -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. /** | */

View file

@ -27,20 +27,22 @@ async function updateConfig(newConfig: VsCodeConfiguration): Promise<VsCodeConfi
}
namespace Config {
export const suggestSelection = 'editor.suggestSelection';
export const completeFunctionCalls = 'typescript.suggest.completeFunctionCalls';
export const autoClosingBrackets = 'editor.autoClosingBrackets';
export const completeFunctionCalls = 'typescript.suggest.completeFunctionCalls';
export const insertMode = 'editor.suggest.insertMode';
export const snippetSuggestions = 'editor.snippetSuggestions';
export const suggestSelection = 'editor.suggestSelection';
}
const insertModes = Object.freeze(['insert', 'replace']);
suite('TypeScript Completions', () => {
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<void>): Promise<void> {