Don't require strict prefix matches for private field completions
Fixxes #89556
This commit is contained in:
parent
b4a835f5b9
commit
58fe34bb77
3 changed files with 149 additions and 25 deletions
|
@ -77,26 +77,8 @@ class MyCompletionItem extends vscode.CompletionItem {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tsEntry.insertText) {
|
|
||||||
this.insertText = tsEntry.insertText;
|
this.insertText = tsEntry.insertText;
|
||||||
|
this.filterText = this.getFilterText(line, 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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (completionContext.isMemberCompletion && completionContext.dotAccessorContext) {
|
if (completionContext.isMemberCompletion && completionContext.dotAccessorContext) {
|
||||||
this.filterText = completionContext.dotAccessorContext.text + (this.insertText || this.label);
|
this.filterText = completionContext.dotAccessorContext.text + (this.insertText || this.label);
|
||||||
|
@ -139,6 +121,42 @@ class MyCompletionItem extends vscode.CompletionItem {
|
||||||
this.resolveRange(line);
|
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 {
|
private resolveRange(line: string): void {
|
||||||
if (this.range) {
|
if (this.range) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -17,7 +17,7 @@ const jsTsLanguageConfiguration: vscode.LanguageConfiguration = {
|
||||||
decreaseIndentPattern: /^((?!.*?\/\*).*\*\/)?\s*[\}\]].*$/,
|
decreaseIndentPattern: /^((?!.*?\/\*).*\*\/)?\s*[\}\]].*$/,
|
||||||
increaseIndentPattern: /^((?!\/\/).)*(\{[^}"'`]*|\([^)"'`]*|\[[^\]"'`]*)$/
|
increaseIndentPattern: /^((?!\/\/).)*(\{[^}"'`]*|\([^)"'`]*|\[[^\]"'`]*)$/
|
||||||
},
|
},
|
||||||
wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,
|
wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,
|
||||||
onEnterRules: [
|
onEnterRules: [
|
||||||
{
|
{
|
||||||
// e.g. /** | */
|
// e.g. /** | */
|
||||||
|
|
|
@ -27,20 +27,22 @@ async function updateConfig(newConfig: VsCodeConfiguration): Promise<VsCodeConfi
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace Config {
|
namespace Config {
|
||||||
export const suggestSelection = 'editor.suggestSelection';
|
|
||||||
export const completeFunctionCalls = 'typescript.suggest.completeFunctionCalls';
|
|
||||||
export const autoClosingBrackets = 'editor.autoClosingBrackets';
|
export const autoClosingBrackets = 'editor.autoClosingBrackets';
|
||||||
|
export const completeFunctionCalls = 'typescript.suggest.completeFunctionCalls';
|
||||||
export const insertMode = 'editor.suggest.insertMode';
|
export const insertMode = 'editor.suggest.insertMode';
|
||||||
|
export const snippetSuggestions = 'editor.snippetSuggestions';
|
||||||
|
export const suggestSelection = 'editor.suggestSelection';
|
||||||
}
|
}
|
||||||
|
|
||||||
const insertModes = Object.freeze(['insert', 'replace']);
|
const insertModes = Object.freeze(['insert', 'replace']);
|
||||||
|
|
||||||
suite('TypeScript Completions', () => {
|
suite('TypeScript Completions', () => {
|
||||||
const configDefaults: VsCodeConfiguration = Object.freeze({
|
const configDefaults: VsCodeConfiguration = Object.freeze({
|
||||||
[Config.suggestSelection]: 'first',
|
|
||||||
[Config.completeFunctionCalls]: false,
|
|
||||||
[Config.autoClosingBrackets]: 'always',
|
[Config.autoClosingBrackets]: 'always',
|
||||||
|
[Config.completeFunctionCalls]: false,
|
||||||
[Config.insertMode]: 'insert',
|
[Config.insertMode]: 'insert',
|
||||||
|
[Config.snippetSuggestions]: 'none',
|
||||||
|
[Config.suggestSelection]: 'first',
|
||||||
});
|
});
|
||||||
|
|
||||||
const _disposables: vscode.Disposable[] = [];
|
const _disposables: vscode.Disposable[] = [];
|
||||||
|
@ -463,6 +465,110 @@ suite('TypeScript Completions', () => {
|
||||||
`abc["xy w"]`
|
`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> {
|
async function enumerateConfig(configKey: string, values: readonly string[], f: (message: string) => Promise<void>): Promise<void> {
|
||||||
|
|
Loading…
Reference in a new issue