diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts
index 252a1a868fa..f289501745a 100644
--- a/src/services/codefixes/importFixes.ts
+++ b/src/services/codefixes/importFixes.ts
@@ -74,7 +74,7 @@ namespace ts.codefix {
const symbolName = getNameForExportedSymbol(exportedSymbol, getEmitScriptTarget(compilerOptions));
const checker = program.getTypeChecker();
const symbol = checker.getMergedSymbol(skipAlias(exportedSymbol, checker));
- const exportInfos = getAllReExportingModules(sourceFile, symbol, moduleSymbol, symbolName, host, program, preferences, useAutoImportProvider);
+ const exportInfos = getAllReExportingModules(sourceFile, symbol, moduleSymbol, symbolName, /*isJsxTagName*/ false, host, program, preferences, useAutoImportProvider);
const useRequire = shouldUseRequire(sourceFile, program);
const fix = getImportFixForSymbol(sourceFile, exportInfos, moduleSymbol, symbolName, program, /*position*/ undefined, !!isValidTypeOnlyUseSite, useRequire, host, preferences);
if (fix) {
@@ -287,6 +287,7 @@ namespace ts.codefix {
moduleSymbol: Symbol,
sourceFile: SourceFile,
symbolName: string,
+ isJsxTagName: boolean,
host: LanguageServiceHost,
program: Program,
formatContext: formatting.FormatContext,
@@ -296,7 +297,7 @@ namespace ts.codefix {
const compilerOptions = program.getCompilerOptions();
const exportInfos = pathIsBareSpecifier(stripQuotes(moduleSymbol.name))
? [getSymbolExportInfoForSymbol(targetSymbol, moduleSymbol, program, host)]
- : getAllReExportingModules(sourceFile, targetSymbol, moduleSymbol, symbolName, host, program, preferences, /*useAutoImportProvider*/ true);
+ : getAllReExportingModules(sourceFile, targetSymbol, moduleSymbol, symbolName, isJsxTagName, host, program, preferences, /*useAutoImportProvider*/ true);
const useRequire = shouldUseRequire(sourceFile, program);
const isValidTypeOnlyUseSite = isValidTypeOnlyAliasUseSite(getTokenAtPosition(sourceFile, position));
const fix = Debug.checkDefined(getImportFixForSymbol(sourceFile, exportInfos, moduleSymbol, symbolName, program, position, isValidTypeOnlyUseSite, useRequire, host, preferences));
@@ -349,7 +350,7 @@ namespace ts.codefix {
}
}
- function getAllReExportingModules(importingFile: SourceFile, targetSymbol: Symbol, exportingModuleSymbol: Symbol, symbolName: string, host: LanguageServiceHost, program: Program, preferences: UserPreferences, useAutoImportProvider: boolean): readonly SymbolExportInfo[] {
+ function getAllReExportingModules(importingFile: SourceFile, targetSymbol: Symbol, exportingModuleSymbol: Symbol, symbolName: string, isJsxTagName: boolean, host: LanguageServiceHost, program: Program, preferences: UserPreferences, useAutoImportProvider: boolean): readonly SymbolExportInfo[] {
const result: SymbolExportInfo[] = [];
const compilerOptions = program.getCompilerOptions();
const getModuleSpecifierResolutionHost = memoizeOne((isFromPackageJson: boolean) => {
@@ -364,7 +365,7 @@ namespace ts.codefix {
}
const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions);
- if (defaultInfo && (defaultInfo.name === symbolName || moduleSymbolToValidIdentifier(moduleSymbol, getEmitScriptTarget(compilerOptions)) === symbolName) && skipAlias(defaultInfo.symbol, checker) === targetSymbol && isImportable(program, moduleFile, isFromPackageJson)) {
+ if (defaultInfo && (defaultInfo.name === symbolName || moduleSymbolToValidIdentifier(moduleSymbol, getEmitScriptTarget(compilerOptions), isJsxTagName) === symbolName) && skipAlias(defaultInfo.symbol, checker) === targetSymbol && isImportable(program, moduleFile, isFromPackageJson)) {
result.push({ symbol: defaultInfo.symbol, moduleSymbol, moduleFileName: moduleFile?.fileName, exportKind: defaultInfo.exportKind, targetFlags: skipAlias(defaultInfo.symbol, checker).flags, isFromPackageJson });
}
@@ -804,7 +805,7 @@ namespace ts.codefix {
const isValidTypeOnlyUseSite = isValidTypeOnlyAliasUseSite(symbolToken);
const useRequire = shouldUseRequire(sourceFile, program);
- const exportInfos = getExportInfos(symbolName, getMeaningFromLocation(symbolToken), cancellationToken, sourceFile, program, useAutoImportProvider, host, preferences);
+ const exportInfos = getExportInfos(symbolName, isJSXTagName(symbolToken), getMeaningFromLocation(symbolToken), cancellationToken, sourceFile, program, useAutoImportProvider, host, preferences);
const fixes = arrayFrom(flatMapIterator(exportInfos.entries(), ([_, exportInfos]) =>
getImportFixes(exportInfos, symbolName, symbolToken.getStart(sourceFile), isValidTypeOnlyUseSite, useRequire, program, sourceFile, host, preferences)));
return { fixes, symbolName };
@@ -845,6 +846,7 @@ namespace ts.codefix {
// Returns a map from an exported symbol's ID to a list of every way it's (re-)exported.
function getExportInfos(
symbolName: string,
+ isJsxTagName: boolean,
currentTokenMeaning: SemanticMeaning,
cancellationToken: CancellationToken,
fromFile: SourceFile,
@@ -876,7 +878,7 @@ namespace ts.codefix {
const compilerOptions = program.getCompilerOptions();
const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions);
- if (defaultInfo && (defaultInfo.name === symbolName || moduleSymbolToValidIdentifier(moduleSymbol, getEmitScriptTarget(compilerOptions)) === symbolName) && symbolHasMeaning(defaultInfo.symbolForMeaning, currentTokenMeaning)) {
+ if (defaultInfo && (defaultInfo.name === symbolName || moduleSymbolToValidIdentifier(moduleSymbol, getEmitScriptTarget(compilerOptions), isJsxTagName) === symbolName) && symbolHasMeaning(defaultInfo.symbolForMeaning, currentTokenMeaning)) {
addSymbol(moduleSymbol, sourceFile, defaultInfo.symbol, defaultInfo.exportKind, program, isFromPackageJson);
}
@@ -1237,17 +1239,20 @@ namespace ts.codefix {
return some(declarations, decl => !!(getMeaningFromDeclaration(decl) & meaning));
}
- export function moduleSymbolToValidIdentifier(moduleSymbol: Symbol, target: ScriptTarget | undefined): string {
- return moduleSpecifierToValidIdentifier(removeFileExtension(stripQuotes(moduleSymbol.name)), target);
+ export function moduleSymbolToValidIdentifier(moduleSymbol: Symbol, target: ScriptTarget | undefined, forceCapitalize: boolean): string {
+ return moduleSpecifierToValidIdentifier(removeFileExtension(stripQuotes(moduleSymbol.name)), target, forceCapitalize);
}
- export function moduleSpecifierToValidIdentifier(moduleSpecifier: string, target: ScriptTarget | undefined): string {
+ export function moduleSpecifierToValidIdentifier(moduleSpecifier: string, target: ScriptTarget | undefined, forceCapitalize?: boolean): string {
const baseName = getBaseFileName(removeSuffix(moduleSpecifier, "/index"));
let res = "";
let lastCharWasValid = true;
const firstCharCode = baseName.charCodeAt(0);
if (isIdentifierStart(firstCharCode, target)) {
res += String.fromCharCode(firstCharCode);
+ if (forceCapitalize) {
+ res = res.toUpperCase();
+ }
}
else {
lastCharWasValid = false;
diff --git a/src/services/completions.ts b/src/services/completions.ts
index fdb76179857..81ff0dd582a 100644
--- a/src/services/completions.ts
+++ b/src/services/completions.ts
@@ -1520,7 +1520,6 @@ namespace ts.Completions {
source: string | undefined,
): CodeActionsAndSourceDisplay {
if (data?.moduleSpecifier) {
- const { contextToken, previousToken } = getRelevantTokens(position, sourceFile);
if (previousToken && getImportStatementCompletionInfo(contextToken || previousToken).replacementNode) {
// Import statement completion: 'import c|'
return { codeActions: undefined, sourceDisplay: [textPart(data.moduleSpecifier)] };
@@ -1572,11 +1571,13 @@ namespace ts.Completions {
const checker = origin.isFromPackageJson ? host.getPackageJsonAutoImportProvider!()!.getTypeChecker() : program.getTypeChecker();
const { moduleSymbol } = origin;
const targetSymbol = checker.getMergedSymbol(skipAlias(symbol.exportSymbol || symbol, checker));
+ const isJsxOpeningTagName = contextToken?.kind === SyntaxKind.LessThanToken && isJsxOpeningLikeElement(contextToken.parent);
const { moduleSpecifier, codeAction } = codefix.getImportCompletionAction(
targetSymbol,
moduleSymbol,
sourceFile,
- getNameForExportedSymbol(symbol, getEmitScriptTarget(compilerOptions)),
+ getNameForExportedSymbol(symbol, getEmitScriptTarget(compilerOptions), isJsxOpeningTagName),
+ isJsxOpeningTagName,
host,
program,
formatContext,
@@ -2486,12 +2487,17 @@ namespace ts.Completions {
preferences,
!!importCompletionNode,
context => {
- exportInfo.forEach(sourceFile.path, (info, symbolName, isFromAmbientModule, exportMapKey) => {
+ exportInfo.forEach(sourceFile.path, (info, getSymbolName, isFromAmbientModule, exportMapKey) => {
+ const symbolName = getSymbolName(/*preferCapitalized*/ isRightOfOpenTag);
if (!isIdentifierText(symbolName, getEmitScriptTarget(host.getCompilationSettings()))) return;
if (!detailsEntryId && isStringANonContextualKeyword(symbolName)) return;
// `targetFlags` should be the same for each `info`
if (!isTypeOnlyLocation && !importCompletionNode && !(info[0].targetFlags & SymbolFlags.Value)) return;
if (isTypeOnlyLocation && !(info[0].targetFlags & (SymbolFlags.Module | SymbolFlags.Type))) return;
+ // Do not try to auto-import something with a lowercase first letter for a JSX tag
+ const firstChar = symbolName.charCodeAt(0);
+ if (isRightOfOpenTag && (firstChar < CharacterCodes.A || firstChar > CharacterCodes.Z)) return;
+
const isCompletionDetailsMatch = detailsEntryId && some(info, i => detailsEntryId.source === stripQuotes(i.moduleSymbol.name));
if (isCompletionDetailsMatch || !detailsEntryId && charactersFuzzyMatchInString(symbolName, lowerCaseTokenText)) {
const defaultExportInfo = find(info, isImportableExportInfo);
@@ -2504,6 +2510,7 @@ namespace ts.Completions {
const { exportInfo = defaultExportInfo, moduleSpecifier } = context.tryResolve(info, isFromAmbientModule) || {};
const isDefaultExport = exportInfo.exportKind === ExportKind.Default;
const symbol = isDefaultExport && getLocalSymbolForExportDefault(exportInfo.symbol) || exportInfo.symbol;
+
pushAutoImportSymbol(symbol, {
kind: moduleSpecifier ? SymbolOriginInfoKind.ResolvedExport : SymbolOriginInfoKind.Export,
moduleSpecifier,
diff --git a/src/services/exportInfoMap.ts b/src/services/exportInfoMap.ts
index 2a43faf3269..ab5de5931de 100644
--- a/src/services/exportInfoMap.ts
+++ b/src/services/exportInfoMap.ts
@@ -45,9 +45,9 @@ namespace ts {
export interface ExportInfoMap {
isUsableByFile(importingFile: Path): boolean;
clear(): void;
- add(importingFile: Path, symbol: Symbol, key: __String, moduleSymbol: Symbol, moduleFile: SourceFile | undefined, exportKind: ExportKind, isFromPackageJson: boolean, scriptTarget: ScriptTarget, checker: TypeChecker): void;
+ add(importingFile: Path, symbol: Symbol, key: __String, moduleSymbol: Symbol, moduleFile: SourceFile | undefined, exportKind: ExportKind, isFromPackageJson: boolean, checker: TypeChecker): void;
get(importingFile: Path, key: string): readonly SymbolExportInfo[] | undefined;
- forEach(importingFile: Path, action: (info: readonly SymbolExportInfo[], name: string, isFromAmbientModule: boolean, key: string) => void): void;
+ forEach(importingFile: Path, action: (info: readonly SymbolExportInfo[], getSymbolName: (preferCapitalized?: boolean) => string, isFromAmbientModule: boolean, key: string) => void): void;
releaseSymbols(): void;
isEmpty(): boolean;
/** @returns Whether the change resulted in the cache being cleared */
@@ -72,7 +72,7 @@ namespace ts {
symbols.clear();
usableByFileName = undefined;
},
- add: (importingFile, symbol, symbolTableKey, moduleSymbol, moduleFile, exportKind, isFromPackageJson, scriptTarget, checker) => {
+ add: (importingFile, symbol, symbolTableKey, moduleSymbol, moduleFile, exportKind, isFromPackageJson, checker) => {
if (importingFile !== usableByFileName) {
cache.clear();
usableByFileName = importingFile;
@@ -88,7 +88,8 @@ namespace ts {
// get a better name.
const importedName = exportKind === ExportKind.Named || isExternalModuleSymbol(namedSymbol)
? unescapeLeadingUnderscores(symbolTableKey)
- : getNameForExportedSymbol(namedSymbol, scriptTarget);
+ : getNameForExportedSymbol(namedSymbol, /*scriptTarget*/ undefined);
+
const moduleName = stripQuotes(moduleSymbol.name);
const id = exportInfoId++;
const target = skipAlias(symbol, checker);
@@ -119,7 +120,18 @@ namespace ts {
if (importingFile !== usableByFileName) return;
exportInfo.forEach((info, key) => {
const { symbolName, ambientModuleName } = parseKey(key);
- action(info.map(rehydrateCachedInfo), symbolName, !!ambientModuleName, key);
+ const rehydrated = info.map(rehydrateCachedInfo);
+ action(
+ rehydrated,
+ preferCapitalized => {
+ const { symbol, exportKind } = rehydrated[0];
+ const namedSymbol = exportKind === ExportKind.Default && getLocalSymbolForExportDefault(symbol) || symbol;
+ return preferCapitalized
+ ? getNameForExportedSymbol(namedSymbol, /*scriptTarget*/ undefined, /*preferCapitalized*/ true)
+ : symbolName;
+ },
+ !!ambientModuleName,
+ key);
});
},
releaseSymbols: () => {
@@ -321,7 +333,6 @@ namespace ts {
host.log?.("getExportInfoMap: cache miss or empty; calculating new results");
const compilerOptions = program.getCompilerOptions();
- const scriptTarget = getEmitScriptTarget(compilerOptions);
let moduleCount = 0;
forEachExternalModuleToImportFrom(program, host, /*useAutoImportProvider*/ true, (moduleSymbol, moduleFile, program, isFromPackageJson) => {
if (++moduleCount % 100 === 0) cancellationToken?.throwIfCancellationRequested();
@@ -339,7 +350,6 @@ namespace ts {
moduleFile,
defaultInfo.exportKind,
isFromPackageJson,
- scriptTarget,
checker);
}
checker.forEachExportAndPropertyOfModule(moduleSymbol, (exported, key) => {
@@ -352,7 +362,6 @@ namespace ts {
moduleFile,
ExportKind.Named,
isFromPackageJson,
- scriptTarget,
checker);
}
});
diff --git a/src/services/utilities.ts b/src/services/utilities.ts
index 58cbbfa7c70..98ac7627e32 100644
--- a/src/services/utilities.ts
+++ b/src/services/utilities.ts
@@ -3210,11 +3210,11 @@ namespace ts {
return isArray(valueOrArray) ? first(valueOrArray) : valueOrArray;
}
- export function getNameForExportedSymbol(symbol: Symbol, scriptTarget: ScriptTarget | undefined) {
+ export function getNameForExportedSymbol(symbol: Symbol, scriptTarget: ScriptTarget | undefined, preferCapitalized?: boolean) {
if (!(symbol.flags & SymbolFlags.Transient) && (symbol.escapedName === InternalSymbolName.ExportEquals || symbol.escapedName === InternalSymbolName.Default)) {
// Name of "export default foo;" is "foo". Name of "export default 0" is the filename converted to camelCase.
return firstDefined(symbol.declarations, d => isExportAssignment(d) ? tryCast(skipOuterExpressions(d.expression), isIdentifier)?.text : undefined)
- || codefix.moduleSymbolToValidIdentifier(getSymbolParentOrFail(symbol), scriptTarget);
+ || codefix.moduleSymbolToValidIdentifier(getSymbolParentOrFail(symbol), scriptTarget, !!preferCapitalized);
}
return symbol.name;
}
diff --git a/src/testRunner/unittests/tsserver/exportMapCache.ts b/src/testRunner/unittests/tsserver/exportMapCache.ts
index 0659c78de27..b082c7f803e 100644
--- a/src/testRunner/unittests/tsserver/exportMapCache.ts
+++ b/src/testRunner/unittests/tsserver/exportMapCache.ts
@@ -87,8 +87,8 @@ namespace ts.projectSystem {
// transient symbols are recreated with every new checker.
const programBefore = project.getCurrentProgram()!;
let sigintPropBefore: readonly SymbolExportInfo[] | undefined;
- exportMapCache.forEach(bTs.path as Path, (info, name) => {
- if (name === "SIGINT") sigintPropBefore = info;
+ exportMapCache.forEach(bTs.path as Path, (info, getSymbolName) => {
+ if (getSymbolName() === "SIGINT") sigintPropBefore = info;
});
assert.ok(sigintPropBefore);
assert.ok(sigintPropBefore![0].symbol.flags & SymbolFlags.Transient);
@@ -113,8 +113,8 @@ namespace ts.projectSystem {
// Get same info from cache again
let sigintPropAfter: readonly SymbolExportInfo[] | undefined;
- exportMapCache.forEach(bTs.path as Path, (info, name) => {
- if (name === "SIGINT") sigintPropAfter = info;
+ exportMapCache.forEach(bTs.path as Path, (info, getSymbolName) => {
+ if (getSymbolName() === "SIGINT") sigintPropAfter = info;
});
assert.ok(sigintPropAfter);
assert.notEqual(symbolIdBefore, getSymbolId(sigintPropAfter![0].symbol));
diff --git a/tests/cases/fourslash/completionsImport_jsxOpeningTagImportDefault.ts b/tests/cases/fourslash/completionsImport_jsxOpeningTagImportDefault.ts
new file mode 100644
index 00000000000..3e183953d50
--- /dev/null
+++ b/tests/cases/fourslash/completionsImport_jsxOpeningTagImportDefault.ts
@@ -0,0 +1,39 @@
+///
+
+// @module: commonjs
+// @jsx: react
+
+// @Filename: /component.tsx
+//// export default function (props: any) {}
+
+// @Filename: /index.tsx
+//// export function Index() {
+//// return
+
+// @module: commonjs
+// @jsx: react-jsx
+
+// @Filename: /component.tsx
+//// export default function (props: any) {}
+
+// @Filename: /index.tsx
+//// export function Index() {
+//// return ;
+//// }
+
+goTo.marker("");
+verify.importFixAtPosition([`import Component from "./component";
+
+export function Index() {
+ return ;
+}`]);