Demote priority of JS completions (#49716)

* Demote priority of JS completions

Fixes #48498

Unchecked JS files gather identifier-based completions. Currently, this search
happens instead of `getCompletionEntriesFromSymbols` for TS/checked JS
files. However, identifier-based completions are much lower quality and
can be ignored by some editors.

Identifier-based completions should be gathered last, after gathering
other completions. That's what this PR does.

* Invert isUncheckedFile to avoid double negative

* dedupe calls to getCompletionEntriesFromSymbols

* Stop re-creating list of entry names

* more deduping + fix lint
This commit is contained in:
Nathan Shively-Sanders 2022-06-29 11:05:50 -07:00 committed by GitHub
parent 63d0574321
commit cba184f69b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 73 additions and 79 deletions

View File

@ -507,87 +507,61 @@ namespace ts.Completions {
}
const entries = createSortedArray<CompletionEntry>();
if (isUncheckedFile(sourceFile, compilerOptions)) {
const uniqueNames = getCompletionEntriesFromSymbols(
symbols,
entries,
/*replacementToken*/ undefined,
contextToken,
location,
sourceFile,
host,
program,
getEmitScriptTarget(compilerOptions),
log,
completionKind,
preferences,
compilerOptions,
formatContext,
isTypeOnlyLocation,
propertyAccessToConvert,
isJsxIdentifierExpected,
isJsxInitializer,
importCompletionNode,
recommendedCompletion,
symbolToOriginInfoMap,
symbolToSortTextMap,
isJsxIdentifierExpected,
isRightOfOpenTag,
);
getJSCompletionEntries(sourceFile, location.pos, uniqueNames, getEmitScriptTarget(compilerOptions), entries);
}
else {
if (!isNewIdentifierLocation && (!symbols || symbols.length === 0) && keywordFilters === KeywordCompletionFilters.None) {
return undefined;
}
getCompletionEntriesFromSymbols(
symbols,
entries,
/*replacementToken*/ undefined,
contextToken,
location,
sourceFile,
host,
program,
getEmitScriptTarget(compilerOptions),
log,
completionKind,
preferences,
compilerOptions,
formatContext,
isTypeOnlyLocation,
propertyAccessToConvert,
isJsxIdentifierExpected,
isJsxInitializer,
importCompletionNode,
recommendedCompletion,
symbolToOriginInfoMap,
symbolToSortTextMap,
isJsxIdentifierExpected,
isRightOfOpenTag,
);
const isChecked = isCheckedFile(sourceFile, compilerOptions);
if (isChecked && !isNewIdentifierLocation && (!symbols || symbols.length === 0) && keywordFilters === KeywordCompletionFilters.None) {
return undefined;
}
const uniqueNames = getCompletionEntriesFromSymbols(
symbols,
entries,
/*replacementToken*/ undefined,
contextToken,
location,
sourceFile,
host,
program,
getEmitScriptTarget(compilerOptions),
log,
completionKind,
preferences,
compilerOptions,
formatContext,
isTypeOnlyLocation,
propertyAccessToConvert,
isJsxIdentifierExpected,
isJsxInitializer,
importCompletionNode,
recommendedCompletion,
symbolToOriginInfoMap,
symbolToSortTextMap,
isJsxIdentifierExpected,
isRightOfOpenTag,
);
if (keywordFilters !== KeywordCompletionFilters.None) {
const entryNames = new Set(entries.map(e => e.name));
for (const keywordEntry of getKeywordCompletions(keywordFilters, !insideJsDocTagTypeExpression && isSourceFileJS(sourceFile))) {
if (isTypeOnlyLocation && isTypeKeyword(stringToToken(keywordEntry.name)!) || !entryNames.has(keywordEntry.name)) {
if (isTypeOnlyLocation && isTypeKeyword(stringToToken(keywordEntry.name)!) || !uniqueNames.has(keywordEntry.name)) {
uniqueNames.add(keywordEntry.name);
insertSorted(entries, keywordEntry, compareCompletionEntries, /*allowDuplicates*/ true);
}
}
}
const entryNames = new Set(entries.map(e => e.name));
for (const keywordEntry of getContextualKeywords(contextToken, position)) {
if (!entryNames.has(keywordEntry.name)) {
if (!uniqueNames.has(keywordEntry.name)) {
uniqueNames.add(keywordEntry.name);
insertSorted(entries, keywordEntry, compareCompletionEntries, /*allowDuplicates*/ true);
}
}
for (const literal of literals) {
insertSorted(entries, createCompletionEntryForLiteral(sourceFile, preferences, literal), compareCompletionEntries, /*allowDuplicates*/ true);
const literalEntry = createCompletionEntryForLiteral(sourceFile, preferences, literal);
uniqueNames.add(literalEntry.name);
insertSorted(entries, literalEntry, compareCompletionEntries, /*allowDuplicates*/ true);
}
if (!isChecked) {
getJSCompletionEntries(sourceFile, location.pos, uniqueNames, getEmitScriptTarget(compilerOptions), entries);
}
return {
@ -601,8 +575,8 @@ namespace ts.Completions {
};
}
function isUncheckedFile(sourceFile: SourceFile, compilerOptions: CompilerOptions): boolean {
return isSourceFileJS(sourceFile) && !isCheckJsEnabledForFile(sourceFile, compilerOptions);
function isCheckedFile(sourceFile: SourceFile, compilerOptions: CompilerOptions): boolean {
return !isSourceFileJS(sourceFile) || !!isCheckJsEnabledForFile(sourceFile, compilerOptions);
}
function isMemberCompletionKind(kind: CompletionKind): boolean {
@ -1942,7 +1916,7 @@ namespace ts.Completions {
cancellationToken?: CancellationToken,
): CompletionData | Request | undefined {
const typeChecker = program.getTypeChecker();
const inUncheckedFile = isUncheckedFile(sourceFile, compilerOptions);
const inCheckedFile = isCheckedFile(sourceFile, compilerOptions);
let start = timestamp();
let currentToken = getTokenAtPosition(sourceFile, position); // TODO: GH#15853
// We will check for jsdoc comments with insideComment and getJsDocTagAtPosition. (TODO: that seems rather inefficient to check the same thing so many times.)
@ -2396,7 +2370,14 @@ namespace ts.Completions {
}
const propertyAccess = node.kind === SyntaxKind.ImportType ? node as ImportTypeNode : node.parent as PropertyAccessExpression | QualifiedName;
if (inUncheckedFile) {
if (inCheckedFile) {
for (const symbol of type.getApparentProperties()) {
if (typeChecker.isValidPropertyAccessForCompletions(propertyAccess, type, symbol)) {
addPropertySymbol(symbol, /* insertAwait */ false, insertQuestionDot);
}
}
}
else {
// In javascript files, for union types, we don't just get the members that
// the individual types have in common, we also include all the members that
// each individual type has. This is because we're going to add all identifiers
@ -2404,13 +2385,6 @@ namespace ts.Completions {
// of the individual types to a higher status since we know what they are.
symbols.push(...filter(getPropertiesForCompletion(type, typeChecker), s => typeChecker.isValidPropertyAccessForCompletions(propertyAccess, type, s)));
}
else {
for (const symbol of type.getApparentProperties()) {
if (typeChecker.isValidPropertyAccessForCompletions(propertyAccess, type, symbol)) {
addPropertySymbol(symbol, /* insertAwait */ false, insertQuestionDot);
}
}
}
if (insertAwait && preferences.includeCompletionsWithInsertText) {
const promiseType = typeChecker.getPromisedTypeOfPromise(type);

View File

@ -5,4 +5,4 @@
/////** @type {function (new: string, string): string} */
////var f = function () { return new/**/; }
verify.completions({ marker: "", includes: { name: "new", sortText: completion.SortText.JavascriptIdentifiers } });
verify.completions({ marker: "", includes: { name: "new", sortText: completion.SortText.GlobalsOrKeywords } });

View File

@ -4,4 +4,4 @@
/////** @type {function (this: string, string): string} */
////var f = function (s) { return this/**/; }
verify.completions({ marker: "", includes: { name: "this", sortText: completion.SortText.JavascriptIdentifiers } });
verify.completions({ marker: "", includes: { name: "this", sortText: completion.SortText.GlobalsOrKeywords } });

View File

@ -0,0 +1,20 @@
/// <reference path="fourslash.ts" />
// @Filename: completionListClassThisJS.js
// @allowJs: true
/////** @typedef {number} CallbackContext */
////class Foo {
//// bar() {
//// this/**/
//// }
//// /** @param {function (this: CallbackContext): any} cb */
//// baz(cb) {
//// }
////}
verify.completions({ marker: "", isNewIdentifierLocation: false, includes: [{
name: "this",
// kind: "warning",
kind: "keyword",
sortText: completion.SortText.GlobalsOrKeywords,
// sortText: completion.SortText.JavascriptIdentifiers,
}] });