diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 44bbfbc9ad5a2..e75346a628764 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -104,10 +104,10 @@ namespace ts { getEmitResolver, getExportsOfModule: getExportsOfModuleAsArray, getAmbientModules, - getJsxElementAttributesType, getJsxIntrinsicTagNames, isOptionalParameter, + tryGetMemberInModuleExports, tryFindAmbientModuleWithoutAugmentations: moduleName => { // we deliberately exclude augmentations // since we are only interested in declarations of the module itself @@ -1483,6 +1483,13 @@ namespace ts { return symbolsToArray(getExportsOfModule(moduleSymbol)); } + function tryGetMemberInModuleExports(memberName: string, moduleSymbol: Symbol): Symbol | undefined { + const symbolTable = getExportsOfModule(moduleSymbol); + if (symbolTable) { + return symbolTable[memberName]; + } + } + function getExportsOfSymbol(symbol: Symbol): SymbolTable { return symbol.flags & SymbolFlags.Module ? getExportsOfModule(symbol) : symbol.exports || emptySymbols; } diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 92feb5414acdb..b64135a5a2c1d 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -1374,6 +1374,14 @@ namespace ts { getEmitScriptTarget(compilerOptions) >= ScriptTarget.ES2015 ? ModuleKind.ES2015 : ModuleKind.CommonJS; } + export function getEmitModuleResolutionKind(compilerOptions: CompilerOptions) { + let moduleResolution = compilerOptions.moduleResolution; + if (moduleResolution === undefined) { + moduleResolution = getEmitModuleKind(compilerOptions) === ModuleKind.CommonJS ? ModuleResolutionKind.NodeJs : ModuleResolutionKind.Classic; + } + return moduleResolution; + } + /* @internal */ export function hasZeroOrOneAsteriskCharacter(str: string): boolean { let seenAsterisk = false; diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index be33335021b51..a4ff852b3be24 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3190,5 +3190,17 @@ "Type '{0}' is not assignable to type '{1}'. Two different types with this name exist, but they are unrelated.": { "category": "Error", "code": 90010 + }, + "Import {0} from {1}": { + "category": "Message", + "code": 90013 + }, + "Change {0} to {1}": { + "category": "Message", + "code": 90014 + }, + "Add {0} to existing import declaration from {1}": { + "category": "Message", + "code": 90015 } } diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts index 2d7f1277776a1..0daca9156d1fc 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -61,7 +61,7 @@ namespace ts { return { resolvedModule: resolved && resolvedModuleFromResolved(resolved, isExternalLibraryImport), failedLookupLocations }; } - function moduleHasNonRelativeName(moduleName: string): boolean { + export function moduleHasNonRelativeName(moduleName: string): boolean { return !(isRootedDiskPath(moduleName) || isExternalModuleNameRelative(moduleName)); } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index b9c4987515855..8df59051ebe90 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2314,6 +2314,8 @@ namespace ts { isOptionalParameter(node: ParameterDeclaration): boolean; getAmbientModules(): Symbol[]; + tryGetMemberInModuleExports(memberName: string, moduleSymbol: Symbol): Symbol | undefined; + /* @internal */ tryFindAmbientModuleWithoutAugmentations(moduleName: string): Symbol; // Should not be called directly. Should only be accessed through the Program instance. @@ -3133,7 +3135,7 @@ namespace ts { target?: ScriptTarget; traceResolution?: boolean; types?: string[]; - /** Paths used to used to compute primary types search locations */ + /** Paths used to compute primary types search locations */ typeRoots?: string[]; /*@internal*/ version?: boolean; /*@internal*/ watch?: boolean; diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index f4fc7b8f009df..1486ea2253f7e 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -2046,6 +2046,34 @@ namespace FourSlash { } } + public verifyImportFixAtPosition(expectedTextArray: string[], errorCode?: number) { + const ranges = this.getRanges(); + if (ranges.length == 0) { + this.raiseError("At least one range should be specified in the testfile."); + } + + const codeFixes = this.getCodeFixes(errorCode); + + if (!codeFixes || codeFixes.length == 0) { + this.raiseError("No codefixes returned."); + } + + const actualTextArray: string[] = []; + const scriptInfo = this.languageServiceAdapterHost.getScriptInfo(codeFixes[0].changes[0].fileName); + const originalContent = scriptInfo.content; + for (const codeFix of codeFixes) { + this.applyEdits(codeFix.changes[0].fileName, codeFix.changes[0].textChanges, /*isFormattingEdit*/ false); + actualTextArray.push(this.normalizeNewlines(this.rangeText(ranges[0]))); + scriptInfo.updateContent(originalContent); + } + const sortedExpectedArray = ts.map(expectedTextArray, str => this.normalizeNewlines(str)).sort(); + const sortedActualArray = actualTextArray.sort(); + if (!ts.arrayIsEqualTo(sortedExpectedArray, sortedActualArray)) { + this.raiseError( + `Actual text array doesn't match expected text array. \nActual: \n"${sortedActualArray.join("\n\n")}"\n---\nExpected: \n'${sortedExpectedArray.join("\n\n")}'`); + } + } + public verifyDocCommentTemplate(expected?: ts.TextInsertion) { const name = "verifyDocCommentTemplate"; const actual = this.languageService.getDocCommentTemplateAtPosition(this.activeFile.fileName, this.currentCaretPosition); @@ -2079,6 +2107,10 @@ namespace FourSlash { }); } + private normalizeNewlines(str: string) { + return str.replace(/\r?\n/g, "\n"); + } + public verifyBraceCompletionAtPosition(negative: boolean, openingBrace: string) { const openBraceMap = ts.createMap({ @@ -2606,7 +2638,7 @@ ${code} resetLocalData(); } - currentFileName = basePath + "/" + value; + currentFileName = ts.isRootedDiskPath(value) ? value : basePath + "/" + value; currentFileOptions[key] = value; } else { @@ -3303,6 +3335,10 @@ namespace FourSlashInterface { this.state.verifyCodeFixAtPosition(expectedText, errorCode); } + public importFixAtPosition(expectedTextArray: string[], errorCode?: number): void { + this.state.verifyImportFixAtPosition(expectedTextArray, errorCode); + } + public navigationBar(json: any) { this.state.verifyNavigationBar(json); } diff --git a/src/services/codefixes/codeFixProvider.ts b/src/services/codefixes/codeFixProvider.ts index c61cbe1b19ea5..e4489fc2dca94 100644 --- a/src/services/codefixes/codeFixProvider.ts +++ b/src/services/codefixes/codeFixProvider.ts @@ -1,4 +1,4 @@ -/* @internal */ +/* @internal */ namespace ts { export interface CodeFix { errorCodes: number[]; @@ -11,6 +11,8 @@ namespace ts { span: TextSpan; program: Program; newLineCharacter: string; + host: LanguageServiceHost; + cancellationToken: CancellationToken; } export namespace codefix { diff --git a/src/services/codefixes/fixes.ts b/src/services/codefixes/fixes.ts index ae13a9658387c..a38b196cb0078 100644 --- a/src/services/codefixes/fixes.ts +++ b/src/services/codefixes/fixes.ts @@ -1,2 +1,3 @@ /// -/// \ No newline at end of file +/// +/// diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts new file mode 100644 index 0000000000000..4adda19689db9 --- /dev/null +++ b/src/services/codefixes/importFixes.ts @@ -0,0 +1,591 @@ +/* @internal */ +namespace ts.codefix { + + type ImportCodeActionKind = "CodeChange" | "InsertingIntoExistingImport" | "NewImport"; + interface ImportCodeAction extends CodeAction { + kind: ImportCodeActionKind, + moduleSpecifier?: string + } + + enum ModuleSpecifierComparison { + Better, + Equal, + Worse + } + + class ImportCodeActionMap { + private symbolIdToActionMap = createMap(); + + addAction(symbolId: number, newAction: ImportCodeAction) { + if (!newAction) { + return; + } + + if (!this.symbolIdToActionMap[symbolId]) { + this.symbolIdToActionMap[symbolId] = [newAction]; + return; + } + + if (newAction.kind === "CodeChange") { + this.symbolIdToActionMap[symbolId].push(newAction); + return; + } + + const updatedNewImports: ImportCodeAction[] = []; + for (const existingAction of this.symbolIdToActionMap[symbolId]) { + if (existingAction.kind === "CodeChange") { + // only import actions should compare + updatedNewImports.push(existingAction); + continue; + } + + switch (this.compareModuleSpecifiers(existingAction.moduleSpecifier, newAction.moduleSpecifier)) { + case ModuleSpecifierComparison.Better: + // the new one is not worth considering if it is a new improt. + // However if it is instead a insertion into existing import, the user might want to use + // the module specifier even it is worse by our standards. So keep it. + if (newAction.kind === "NewImport") { + return; + } + case ModuleSpecifierComparison.Equal: + // the current one is safe. But it is still possible that the new one is worse + // than another existing one. For example, you may have new imports from "./foo/bar" + // and "bar", when the new one is "bar/bar2" and the current one is "./foo/bar". The new + // one and the current one are not comparable (one relative path and one absolute path), + // but the new one is worse than the other one, so should not add to the list. + updatedNewImports.push(existingAction); + break; + case ModuleSpecifierComparison.Worse: + // the existing one is worse, remove from the list. + continue; + } + } + // if we reach here, it means the new one is better or equal to all of the existing ones. + updatedNewImports.push(newAction); + this.symbolIdToActionMap[symbolId] = updatedNewImports; + } + + addActions(symbolId: number, newActions: ImportCodeAction[]) { + for (const newAction of newActions) { + this.addAction(symbolId, newAction); + } + } + + getAllActions() { + let result: ImportCodeAction[] = []; + for (const symbolId in this.symbolIdToActionMap) { + result = concatenate(result, this.symbolIdToActionMap[symbolId]); + } + return result; + } + + private compareModuleSpecifiers(moduleSpecifier1: string, moduleSpecifier2: string): ModuleSpecifierComparison { + if (moduleSpecifier1 === moduleSpecifier2) { + return ModuleSpecifierComparison.Equal; + } + + // if moduleSpecifier1 (ms1) is a substring of ms2, then it is better + if (moduleSpecifier2.indexOf(moduleSpecifier1) === 0) { + return ModuleSpecifierComparison.Better; + } + + if (moduleSpecifier1.indexOf(moduleSpecifier2) === 0) { + return ModuleSpecifierComparison.Worse; + } + + // if both are relative paths, and ms1 has fewer levels, then it is better + if (isExternalModuleNameRelative(moduleSpecifier1) && isExternalModuleNameRelative(moduleSpecifier2)) { + const regex = new RegExp(directorySeparator, "g"); + const moduleSpecifier1LevelCount = (moduleSpecifier1.match(regex) || []).length; + const moduleSpecifier2LevelCount = (moduleSpecifier2.match(regex) || []).length; + + return moduleSpecifier1LevelCount < moduleSpecifier2LevelCount + ? ModuleSpecifierComparison.Better + : moduleSpecifier1LevelCount === moduleSpecifier2LevelCount + ? ModuleSpecifierComparison.Equal + : ModuleSpecifierComparison.Worse; + } + + // the equal cases include when the two specifiers are not comparable. + return ModuleSpecifierComparison.Equal; + } + } + + registerCodeFix({ + errorCodes: [Diagnostics.Cannot_find_name_0.code], + getCodeActions: (context: CodeFixContext) => { + const sourceFile = context.sourceFile; + const checker = context.program.getTypeChecker(); + const allSourceFiles = context.program.getSourceFiles(); + const useCaseSensitiveFileNames = context.host.useCaseSensitiveFileNames ? context.host.useCaseSensitiveFileNames() : false; + + const token = getTokenAtPosition(sourceFile, context.span.start); + const name = token.getText(); + const symbolIdActionMap = new ImportCodeActionMap(); + + // this is a module id -> module import declaration map + const cachedImportDeclarations = createMap<(ImportDeclaration | ImportEqualsDeclaration)[]>(); + let cachedNewImportInsertPosition: number; + + const allPotentialModules = checker.getAmbientModules(); + for (const otherSourceFile of allSourceFiles) { + if (otherSourceFile !== sourceFile && isExternalOrCommonJsModule(otherSourceFile)) { + allPotentialModules.push(otherSourceFile.symbol); + } + } + + const currentTokenMeaning = getMeaningFromLocation(token); + for (const moduleSymbol of allPotentialModules) { + context.cancellationToken.throwIfCancellationRequested(); + + // check the default export + const defaultExport = checker.tryGetMemberInModuleExports("default", moduleSymbol); + if (defaultExport) { + const localSymbol = getLocalSymbolForExportDefault(defaultExport); + if (localSymbol && localSymbol.name === name && checkSymbolHasMeaning(localSymbol, currentTokenMeaning)) { + // check if this symbol is already used + const symbolId = getUniqueSymbolId(localSymbol); + symbolIdActionMap.addActions(symbolId, getCodeActionForImport(moduleSymbol, /*isDefaultExport*/ true)); + } + } + + // check exports with the same name + const exportSymbolWithIdenticalName = checker.tryGetMemberInModuleExports(name, moduleSymbol); + if (exportSymbolWithIdenticalName && checkSymbolHasMeaning(exportSymbolWithIdenticalName, currentTokenMeaning)) { + const symbolId = getUniqueSymbolId(exportSymbolWithIdenticalName); + symbolIdActionMap.addActions(symbolId, getCodeActionForImport(moduleSymbol)); + } + } + + return symbolIdActionMap.getAllActions(); + + function getImportDeclarations(moduleSymbol: Symbol) { + const moduleSymbolId = getUniqueSymbolId(moduleSymbol); + + if (cachedImportDeclarations[moduleSymbolId]) { + return cachedImportDeclarations[moduleSymbolId]; + } + + const existingDeclarations: (ImportDeclaration | ImportEqualsDeclaration)[] = []; + for (const importModuleSpecifier of sourceFile.imports) { + const importSymbol = checker.getSymbolAtLocation(importModuleSpecifier); + if (importSymbol === moduleSymbol) { + existingDeclarations.push(getImportDeclaration(importModuleSpecifier)); + } + } + cachedImportDeclarations[moduleSymbolId] = existingDeclarations; + return existingDeclarations; + + function getImportDeclaration(moduleSpecifier: LiteralExpression) { + let node: Node = moduleSpecifier; + while (node) { + if (node.kind === SyntaxKind.ImportDeclaration) { + return node; + } + if (node.kind === SyntaxKind.ImportEqualsDeclaration) { + return node; + } + node = node.parent; + } + return undefined; + } + } + + function getUniqueSymbolId(symbol: Symbol) { + if (symbol.flags & SymbolFlags.Alias) { + return getSymbolId(checker.getAliasedSymbol(symbol)); + } + return getSymbolId(symbol); + } + + function checkSymbolHasMeaning(symbol: Symbol, meaning: SemanticMeaning) { + const declarations = symbol.getDeclarations(); + return declarations ? some(symbol.declarations, decl => !!(getMeaningFromDeclaration(decl) & meaning)) : false; + } + + function getCodeActionForImport(moduleSymbol: Symbol, isDefault?: boolean): ImportCodeAction[] { + const existingDeclarations = getImportDeclarations(moduleSymbol); + if (existingDeclarations.length > 0) { + // With an existing import statement, there are more than one actions the user can do. + return getCodeActionsForExistingImport(existingDeclarations); + } + else { + return [getCodeActionForNewImport()]; + } + + + + function getCodeActionsForExistingImport(declarations: (ImportDeclaration | ImportEqualsDeclaration)[]): ImportCodeAction[] { + const actions: ImportCodeAction[] = []; + + // It is possible that multiple import statements with the same specifier exist in the file. + // e.g. + // + // import * as ns from "foo"; + // import { member1, member2 } from "foo"; + // + // member3/**/ <-- cusor here + // + // in this case we should provie 2 actions: + // 1. change "member3" to "ns.member3" + // 2. add "member3" to the second import statement's import list + // and it is up to the user to decide which one fits best. + let namespaceImportDeclaration: ImportDeclaration | ImportEqualsDeclaration; + let namedImportDeclaration: ImportDeclaration; + let existingModuleSpecifier: string; + for (const declaration of declarations) { + if (declaration.kind === SyntaxKind.ImportDeclaration) { + const namedBindings = declaration.importClause && declaration.importClause.namedBindings; + if (namedBindings && namedBindings.kind === SyntaxKind.NamespaceImport) { + // case: + // import * as ns from "foo" + namespaceImportDeclaration = declaration; + } + else { + // cases: + // import default from "foo" + // import { bar } from "foo" or combination with the first one + // import "foo" + namedImportDeclaration = declaration; + } + existingModuleSpecifier = declaration.moduleSpecifier.getText(); + } + else { + // case: + // import foo = require("foo") + namespaceImportDeclaration = declaration; + existingModuleSpecifier = getModuleSpecifierFromImportEqualsDeclaration(declaration); + } + } + + if (namespaceImportDeclaration) { + actions.push(getCodeActionForNamespaceImport(namespaceImportDeclaration)); + } + + if (namedImportDeclaration && namedImportDeclaration.importClause && + (namedImportDeclaration.importClause.name || namedImportDeclaration.importClause.namedBindings)) { + /** + * If the existing import declaration already has a named import list, just + * insert the identifier into that list. + */ + const textChange = getTextChangeForImportClause(namedImportDeclaration.importClause); + const moduleSpecifierWithoutQuotes = stripQuotes(namedImportDeclaration.moduleSpecifier.getText()); + actions.push(createCodeAction( + Diagnostics.Add_0_to_existing_import_declaration_from_1, + [name, moduleSpecifierWithoutQuotes], + textChange.newText, + textChange.span, + sourceFile.fileName, + "InsertingIntoExistingImport", + moduleSpecifierWithoutQuotes + )); + } + else { + // we need to create a new import statement, but the existing module specifier can be reused. + actions.push(getCodeActionForNewImport(existingModuleSpecifier)); + } + return actions; + + function getModuleSpecifierFromImportEqualsDeclaration(declaration: ImportEqualsDeclaration) { + if (declaration.moduleReference && declaration.moduleReference.kind === SyntaxKind.ExternalModuleReference) { + return declaration.moduleReference.expression.getText(); + } + return declaration.moduleReference.getText(); + } + + function getTextChangeForImportClause(importClause: ImportClause): TextChange { + const newImportText = isDefault ? `default as ${name}` : name; + const importList = importClause.namedBindings; + // case 1: + // original text: import default from "module" + // change to: import default, { name } from "module" + if (!importList && importClause.name) { + const start = importClause.name.getEnd(); + return { + newText: `, { ${newImportText} }`, + span: { start, length: 0 } + }; + } + + // case 2: + // original text: import {} from "module" + // change to: import { name } from "module" + if (importList.elements.length === 0) { + const start = importList.getStart(); + return { + newText: `{ ${newImportText} }`, + span: { start, length: importList.getEnd() - start } + }; + } + + // case 3: + // original text: import { foo, bar } from "module" + // change to: import { foo, bar, name } from "module" + const insertPoint = importList.elements[importList.elements.length - 1].getEnd(); + /** + * If the import list has one import per line, preserve that. Otherwise, insert on same line as last element + * import { + * foo + * } from "./module"; + */ + const startLine = getLineOfLocalPosition(sourceFile, importList.getStart()); + const endLine = getLineOfLocalPosition(sourceFile, importList.getEnd()); + const oneImportPerLine = endLine - startLine > importList.elements.length; + + return { + newText: `,${oneImportPerLine ? context.newLineCharacter : " "}${newImportText}`, + span: { start: insertPoint, length: 0 } + }; + } + + function getCodeActionForNamespaceImport(declaration: ImportDeclaration | ImportEqualsDeclaration): ImportCodeAction { + let namespacePrefix: string; + if (declaration.kind === SyntaxKind.ImportDeclaration) { + namespacePrefix = (declaration.importClause.namedBindings).name.getText(); + } + else { + namespacePrefix = declaration.name.getText(); + } + namespacePrefix = stripQuotes(namespacePrefix); + + /** + * Cases: + * import * as ns from "mod" + * import default, * as ns from "mod" + * import ns = require("mod") + * + * Because there is no import list, we alter the reference to include the + * namespace instead of altering the import declaration. For example, "foo" would + * become "ns.foo" + */ + return createCodeAction( + Diagnostics.Change_0_to_1, + [name, `${namespacePrefix}.${name}`], + `${namespacePrefix}.`, + { start: token.getStart(), length: 0 }, + sourceFile.fileName, + "CodeChange" + ); + } + } + + function getCodeActionForNewImport(moduleSpecifier?: string): ImportCodeAction { + if (!cachedNewImportInsertPosition) { + // insert after any existing imports + let lastModuleSpecifierEnd = -1; + for (const moduleSpecifier of sourceFile.imports) { + const end = moduleSpecifier.getEnd(); + if (!lastModuleSpecifierEnd || end > lastModuleSpecifierEnd) { + lastModuleSpecifierEnd = end; + } + } + cachedNewImportInsertPosition = lastModuleSpecifierEnd > 0 ? sourceFile.getLineEndOfPosition(lastModuleSpecifierEnd) : sourceFile.getStart(); + } + + const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); + const moduleSpecifierWithoutQuotes = stripQuotes(moduleSpecifier || getModuleSpecifierForNewImport()); + const importStatementText = isDefault + ? `import ${name} from "${moduleSpecifierWithoutQuotes}"` + : `import { ${name} } from "${moduleSpecifierWithoutQuotes}"`; + + // if this file doesn't have any import statements, insert an import statement and then insert a new line + // between the only import statement and user code. Otherwise just insert the statement because chances + // are there are already a new line seperating code and import statements. + const newText = cachedNewImportInsertPosition === sourceFile.getStart() + ? `${importStatementText};${context.newLineCharacter}${context.newLineCharacter}` + : `${context.newLineCharacter}${importStatementText};`; + + return createCodeAction( + Diagnostics.Import_0_from_1, + [name, `"${moduleSpecifierWithoutQuotes}"`], + newText, + { start: cachedNewImportInsertPosition, length: 0 }, + sourceFile.fileName, + "NewImport", + moduleSpecifierWithoutQuotes + ); + + function getModuleSpecifierForNewImport() { + const fileName = sourceFile.path; + const moduleFileName = moduleSymbol.valueDeclaration.getSourceFile().path; + const sourceDirectory = getDirectoryPath(fileName); + const options = context.program.getCompilerOptions(); + + return tryGetModuleNameFromAmbientModule() || + tryGetModuleNameFromBaseUrl() || + tryGetModuleNameFromRootDirs() || + tryGetModuleNameFromTypeRoots() || + tryGetModuleNameAsNodeModule() || + removeFileExtension(getRelativePath(moduleFileName, sourceDirectory)); + + function tryGetModuleNameFromAmbientModule(): string { + if (moduleSymbol.valueDeclaration.kind !== SyntaxKind.SourceFile) { + return moduleSymbol.name; + } + } + + function tryGetModuleNameFromBaseUrl() { + if (!options.baseUrl) { + return undefined; + } + + const normalizedBaseUrl = toPath(options.baseUrl, getDirectoryPath(options.baseUrl), getCanonicalFileName); + let relativeName = tryRemoveParentDirectoryName(moduleFileName, normalizedBaseUrl); + if (!relativeName) { + return undefined; + } + + relativeName = removeExtensionAndIndexPostFix(relativeName); + + if (options.paths) { + for (const key in options.paths) { + for (const pattern of options.paths[key]) { + const indexOfStar = pattern.indexOf("*"); + if (indexOfStar === 0 && pattern.length === 1) { + continue; + } + else if (indexOfStar !== -1) { + const prefix = pattern.substr(0, indexOfStar); + const suffix = pattern.substr(indexOfStar + 1); + if (relativeName.length >= prefix.length + suffix.length && + startsWith(relativeName, prefix) && + endsWith(relativeName, suffix)) { + const matchedStar = relativeName.substr(prefix.length, relativeName.length - suffix.length); + return key.replace("\*", matchedStar); + } + } + else if (pattern === relativeName) { + return key; + } + } + } + } + + return relativeName; + } + + function tryGetModuleNameFromRootDirs() { + if (options.rootDirs) { + const normalizedRootDirs = map(options.rootDirs, rootDir => toPath(rootDir, /*basePath*/ undefined, getCanonicalFileName)); + const normalizedTargetPath = getPathRelativeToRootDirs(moduleFileName, normalizedRootDirs); + const normalizedSourcePath = getPathRelativeToRootDirs(sourceDirectory, normalizedRootDirs); + if (normalizedTargetPath !== undefined) { + const relativePath = normalizedSourcePath !== undefined ? getRelativePath(normalizedTargetPath, normalizedSourcePath) : normalizedTargetPath; + return removeFileExtension(relativePath); + } + } + return undefined; + } + + function tryGetModuleNameFromTypeRoots() { + const typeRoots = getEffectiveTypeRoots(options, context.host); + if (typeRoots) { + const normalizedTypeRoots = map(typeRoots, typeRoot => toPath(typeRoot, /*basePath*/ undefined, getCanonicalFileName)); + for (const typeRoot of normalizedTypeRoots) { + if (startsWith(moduleFileName, typeRoot)) { + let relativeFileName = moduleFileName.substring(typeRoot.length + 1); + return removeExtensionAndIndexPostFix(relativeFileName); + } + } + } + } + + function tryGetModuleNameAsNodeModule() { + if (getEmitModuleResolutionKind(options) !== ModuleResolutionKind.NodeJs) { + // nothing to do here + return undefined; + } + + const indexOfNodeModules = moduleFileName.indexOf("node_modules"); + if (indexOfNodeModules < 0) { + return undefined; + } + + let relativeFileName: string; + if (sourceDirectory.indexOf(moduleFileName.substring(0, indexOfNodeModules - 1)) === 0) { + // if node_modules folder is in this folder or any of its parent folder, no need to keep it. + relativeFileName = moduleFileName.substring(indexOfNodeModules + 13 /* "node_modules\".length */); + } + else { + relativeFileName = getRelativePath(moduleFileName, sourceDirectory); + } + + relativeFileName = removeFileExtension(relativeFileName); + if (endsWith(relativeFileName, "/index")) { + relativeFileName = getDirectoryPath(relativeFileName); + } + else { + try { + const moduleDirectory = getDirectoryPath(moduleFileName); + const packageJsonContent = JSON.parse(context.host.readFile(combinePaths(moduleDirectory, "package.json"))); + if (packageJsonContent) { + const mainFile = packageJsonContent.main || packageJsonContent.typings; + if (mainFile) { + const mainExportFile = toPath(mainFile, moduleDirectory, getCanonicalFileName); + if (removeFileExtension(mainExportFile) === removeFileExtension(moduleFileName)) { + relativeFileName = getDirectoryPath(relativeFileName); + } + } + } + } + catch (e) { } + } + + return relativeFileName; + } + } + + function getPathRelativeToRootDirs(path: Path, rootDirs: Path[]) { + for (const rootDir of rootDirs) { + const relativeName = tryRemoveParentDirectoryName(path, rootDir); + if (relativeName !== undefined) { + return relativeName; + } + } + return undefined; + } + + function removeExtensionAndIndexPostFix(fileName: string) { + fileName = removeFileExtension(fileName); + if (endsWith(fileName, "/index")) { + fileName = fileName.substr(0, fileName.length - 6/* "/index".length */); + } + return fileName; + } + + function getRelativePath(path: string, directoryPath: string) { + const relativePath = getRelativePathToDirectoryOrUrl(directoryPath, path, directoryPath, getCanonicalFileName, false); + return moduleHasNonRelativeName(relativePath) ? "./" + relativePath : relativePath; + } + + function tryRemoveParentDirectoryName(path: Path, parentDirectory: Path) { + const index = path.indexOf(parentDirectory); + if (index === 0) { + return endsWith(parentDirectory, directorySeparator) + ? path.substring(parentDirectory.length) + : path.substring(parentDirectory.length + 1); + } + return undefined; + } + } + + } + + function createCodeAction( + description: DiagnosticMessage, + diagnosticArgs: string[], + newText: string, + span: TextSpan, + fileName: string, + kind: ImportCodeActionKind, + moduleSpecifier?: string): ImportCodeAction { + return { + description: formatMessage.apply(undefined, [undefined, description].concat(diagnosticArgs)), + changes: [{ fileName, textChanges: [{ newText, span }] }], + kind, + moduleSpecifier + }; + } + } + }); +} diff --git a/src/services/services.ts b/src/services/services.ts index 3c0b78383141d..ef50f4b5a2f3a 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1,4 +1,4 @@ -/// +/// /// /// @@ -492,6 +492,23 @@ namespace ts { return ts.getPositionOfLineAndCharacter(this, line, character); } + public getLineEndOfPosition(pos: number): number { + const { line } = this.getLineAndCharacterOfPosition(pos); + const lineStarts = this.getLineStarts(); + + let lastCharPos: number; + if (line + 1 >= lineStarts.length) { + lastCharPos = this.getEnd(); + } + if (!lastCharPos) { + lastCharPos = lineStarts[line + 1] - 1; + } + + const fullText = this.getFullText(); + // if the new line is "\r\n", we should return the last non-new-line-character position + return fullText[lastCharPos] === "\n" && fullText[lastCharPos - 1] === "\r" ? lastCharPos - 1 : lastCharPos; + } + public getNamedDeclarations(): Map { if (!this.namedDeclarations) { this.namedDeclarations = this.computeNamedDeclarations(); @@ -1676,7 +1693,9 @@ namespace ts { sourceFile: sourceFile, span: span, program: program, - newLineCharacter: newLineChar + newLineCharacter: newLineChar, + host: host, + cancellationToken: cancellationToken }; const fixes = codefix.getFixes(context); diff --git a/src/services/types.ts b/src/services/types.ts index 4e04df3fc7caf..6a0e6e886b583 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -53,6 +53,7 @@ namespace ts { /* @internal */ getNamedDeclarations(): Map; getLineAndCharacterOfPosition(pos: number): LineAndCharacter; + getLineEndOfPosition(pos: number): number; getLineStarts(): number[]; getPositionOfLineAndCharacter(line: number, character: number): number; update(newText: string, textChangeRange: TextChangeRange): SourceFile; diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 295b8e422b9ba..35c68b49fd88c 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -211,6 +211,7 @@ declare namespace FourSlashInterface { DocCommentTemplate(expectedText: string, expectedOffset: number, empty?: boolean): void; noDocCommentTemplate(): void; codeFixAtPosition(expectedText: string, errorCode?: number): void; + importFixAtPosition(expectedTextArray: string[], errorCode?: number): void; navigationBar(json: any): void; navigationTree(json: any): void; diff --git a/tests/cases/fourslash/importNameCodeFixExistingImport0.ts b/tests/cases/fourslash/importNameCodeFixExistingImport0.ts new file mode 100644 index 0000000000000..5e5be220688c7 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFixExistingImport0.ts @@ -0,0 +1,10 @@ +/// + +//// import [|{ v1 }|] from "./module"; +//// f1/*0*/(); + +// @Filename: module.ts +//// export function f1() {} +//// export var v1 = 5; + +verify.importFixAtPosition([`{ v1, f1 }`]); \ No newline at end of file diff --git a/tests/cases/fourslash/importNameCodeFixExistingImport1.ts b/tests/cases/fourslash/importNameCodeFixExistingImport1.ts new file mode 100644 index 0000000000000..9571d0fcf57cc --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFixExistingImport1.ts @@ -0,0 +1,11 @@ +/// + +//// import d, [|{ v1 }|] from "./module"; +//// f1/*0*/(); + +// @Filename: module.ts +//// export function f1() {} +//// export var v1 = 5; +//// export default var d1 = 6; + +verify.importFixAtPosition([`{ v1, f1 }`]); \ No newline at end of file diff --git a/tests/cases/fourslash/importNameCodeFixExistingImport10.ts b/tests/cases/fourslash/importNameCodeFixExistingImport10.ts new file mode 100644 index 0000000000000..25246e7012328 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFixExistingImport10.ts @@ -0,0 +1,21 @@ +/// + +//// import [|{ +//// v1, +//// v2 +//// }|] from "./module"; +//// f1/*0*/(); + +// @Filename: module.ts +//// export function f1() {} +//// export var v1 = 5; +//// export var v2 = 5; +//// export var v3 = 5; + +verify.importFixAtPosition([ +`{ + v1, + v2, +f1 +}` +]); \ No newline at end of file diff --git a/tests/cases/fourslash/importNameCodeFixExistingImport11.ts b/tests/cases/fourslash/importNameCodeFixExistingImport11.ts new file mode 100644 index 0000000000000..304ffb896dfb5 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFixExistingImport11.ts @@ -0,0 +1,20 @@ +/// + +////import [|{ +//// v1, v2, +//// v3 +////}|] from "./module"; +////f1/*0*/(); + +// @Filename: module.ts +//// export function f1() {} +//// export var v1 = 5; +//// export var v2 = 5; +//// export var v3 = 5; + +verify.importFixAtPosition([ +`{ + v1, v2, + v3, f1 +}` +]); \ No newline at end of file diff --git a/tests/cases/fourslash/importNameCodeFixExistingImport12.ts b/tests/cases/fourslash/importNameCodeFixExistingImport12.ts new file mode 100644 index 0000000000000..e00dee504c5f2 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFixExistingImport12.ts @@ -0,0 +1,12 @@ +/// + +//// import [|{}|] from "./module"; +//// f1/*0*/(); + +// @Filename: module.ts +//// export function f1() {} +//// export var v1 = 5; +//// export var v2 = 5; +//// export var v3 = 5; + +verify.importFixAtPosition([`{ f1 }`]); \ No newline at end of file diff --git a/tests/cases/fourslash/importNameCodeFixExistingImport2.ts b/tests/cases/fourslash/importNameCodeFixExistingImport2.ts new file mode 100644 index 0000000000000..6a92976f4ef68 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFixExistingImport2.ts @@ -0,0 +1,16 @@ +/// + +//// [|import * as ns from "./module"; +//// f1/*0*/();|] + +// @Filename: module.ts +//// export function f1() {} +//// export var v1 = 5; + +verify.importFixAtPosition([ +`import * as ns from "./module"; +import { f1 } from "./module"; +f1();`, +`import * as ns from "./module"; +ns.f1();` +]); \ No newline at end of file diff --git a/tests/cases/fourslash/importNameCodeFixExistingImport3.ts b/tests/cases/fourslash/importNameCodeFixExistingImport3.ts new file mode 100644 index 0000000000000..bc00e8d420a07 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFixExistingImport3.ts @@ -0,0 +1,18 @@ +/// + +//// [|import d, * as ns from "./module" ; +//// f1/*0*/();|] + +// @Filename: module.ts +//// export function f1() {} +//// export var v1 = 5; +//// export default var d1 = 6; + +// Test with some extra spaces before the semicolon +verify.importFixAtPosition([ +`import d, * as ns from "./module" ; +ns.f1();`, +`import d, * as ns from "./module" ; +import { f1 } from "./module"; +f1();`, +]); \ No newline at end of file diff --git a/tests/cases/fourslash/importNameCodeFixExistingImport4.ts b/tests/cases/fourslash/importNameCodeFixExistingImport4.ts new file mode 100644 index 0000000000000..d93cb73664e4c --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFixExistingImport4.ts @@ -0,0 +1,14 @@ +/// + +//// [|import d from "./module"; +//// f1/*0*/();|] + +// @Filename: module.ts +//// export function f1() {} +//// export var v1 = 5; +//// export default var d1 = 6; + +verify.importFixAtPosition([ +`import d, { f1 } from "./module"; +f1();` +]); \ No newline at end of file diff --git a/tests/cases/fourslash/importNameCodeFixExistingImport5.ts b/tests/cases/fourslash/importNameCodeFixExistingImport5.ts new file mode 100644 index 0000000000000..ed9297124d936 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFixExistingImport5.ts @@ -0,0 +1,12 @@ +/// + +//// [|import "./module"; +//// f1/*0*/();|] + +// @Filename: module.ts +//// export function f1() {} +//// export var v1 = 5; + +verify.importFixAtPosition([`import "./module"; +import { f1 } from "./module"; +f1();`]); \ No newline at end of file diff --git a/tests/cases/fourslash/importNameCodeFixExistingImport6.ts b/tests/cases/fourslash/importNameCodeFixExistingImport6.ts new file mode 100644 index 0000000000000..7ae157a51ceb9 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFixExistingImport6.ts @@ -0,0 +1,13 @@ +/// + +//// import [|{ v1 }|] from "fake-module"; +//// f1/*0*/(); + +// @Filename: ../package.json +//// { "dependencies": { "fake-module": "latest" } } + +// @Filename: ../node_modules/fake-module/index.ts +//// export var v1 = 5; +//// export function f1(); + +verify.importFixAtPosition([`{ v1, f1 }`]); \ No newline at end of file diff --git a/tests/cases/fourslash/importNameCodeFixExistingImport7.ts b/tests/cases/fourslash/importNameCodeFixExistingImport7.ts new file mode 100644 index 0000000000000..249929eabc7a0 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFixExistingImport7.ts @@ -0,0 +1,10 @@ +/// + +//// import [|{ v1 }|] from "../other_dir/module"; +//// f1/*0*/(); + +// @Filename: ../other_dir/module.ts +//// export var v1 = 5; +//// export function f1(); + +verify.importFixAtPosition([`{ v1, f1 }`]); \ No newline at end of file diff --git a/tests/cases/fourslash/importNameCodeFixExistingImport8.ts b/tests/cases/fourslash/importNameCodeFixExistingImport8.ts new file mode 100644 index 0000000000000..da7beaa0a4769 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFixExistingImport8.ts @@ -0,0 +1,12 @@ +/// + +//// import [|{v1, v2, v3,}|] from "./module"; +//// f1/*0*/(); + +// @Filename: module.ts +//// export function f1() {} +//// export var v1 = 5; +//// export var v2 = 5; +//// export var v3 = 5; + +verify.importFixAtPosition([`{v1, v2, v3, f1,}`]); \ No newline at end of file diff --git a/tests/cases/fourslash/importNameCodeFixExistingImport9.ts b/tests/cases/fourslash/importNameCodeFixExistingImport9.ts new file mode 100644 index 0000000000000..05d179274548f --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFixExistingImport9.ts @@ -0,0 +1,17 @@ +/// + +//// import [|{ +//// v1 +//// }|] from "./module"; +//// f1/*0*/(); + +// @Filename: module.ts +//// export function f1() {} +//// export var v1 = 5; + +verify.importFixAtPosition([ +`{ + v1, +f1 +}` +]); \ No newline at end of file diff --git a/tests/cases/fourslash/importNameCodeFixExistingImportEquals0.ts b/tests/cases/fourslash/importNameCodeFixExistingImportEquals0.ts new file mode 100644 index 0000000000000..f431e6356d126 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFixExistingImportEquals0.ts @@ -0,0 +1,18 @@ +/// + +//// [|import ns = require("ambient-module"); +//// var x = v1/*0*/ + 5;|] + +// @Filename: ambientModule.ts +//// declare module "ambient-module" { +//// export function f1(); +//// export var v1; +//// } + +verify.importFixAtPosition([ +`import ns = require("ambient-module"); +var x = ns.v1 + 5;`, +`import ns = require("ambient-module"); +import { v1 } from "ambient-module"; +var x = v1 + 5;`, +]); \ No newline at end of file diff --git a/tests/cases/fourslash/importNameCodeFixNewImportAmbient0.ts b/tests/cases/fourslash/importNameCodeFixNewImportAmbient0.ts new file mode 100644 index 0000000000000..1d7b5bc3e7f33 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFixNewImportAmbient0.ts @@ -0,0 +1,15 @@ +/// + +//// [|f1/*0*/();|] + +// @Filename: ambientModule.ts +//// declare module "ambient-module" { +//// export function f1(); +//// export var v1; +//// } + +verify.importFixAtPosition([ +`import { f1 } from "ambient-module"; + +f1();` +]); \ No newline at end of file diff --git a/tests/cases/fourslash/importNameCodeFixNewImportAmbient1.ts b/tests/cases/fourslash/importNameCodeFixNewImportAmbient1.ts new file mode 100644 index 0000000000000..60504c8971165 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFixNewImportAmbient1.ts @@ -0,0 +1,28 @@ +/// + +//// import d from "other-ambient-module"; +//// [|import * as ns from "yet-another-ambient-module"; +//// var x = v1/*0*/ + 5;|] + +// @Filename: ambientModule.ts +//// declare module "ambient-module" { +//// export function f1(); +//// export var v1; +//// } + +// @Filename: otherAmbientModule.ts +//// declare module "other-ambient-module" { +//// export default function f2(); +//// } + +// @Filename: yetAnotherAmbientModule.ts +//// declare module "yet-another-ambient-module" { +//// export function f3(); +//// export var v3; +//// } + +verify.importFixAtPosition([ +`import * as ns from "yet-another-ambient-module"; +import { v1 } from "ambient-module"; +var x = v1 + 5;` +]); \ No newline at end of file diff --git a/tests/cases/fourslash/importNameCodeFixNewImportAmbient2.ts b/tests/cases/fourslash/importNameCodeFixNewImportAmbient2.ts new file mode 100644 index 0000000000000..999da4bffbbde --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFixNewImportAmbient2.ts @@ -0,0 +1,21 @@ +/// + +////[|/* +//// * I'm a license or something +//// */ +////f1/*0*/();|] + +// @Filename: ambientModule.ts +//// declare module "ambient-module" { +//// export function f1(); +//// export var v1; +//// } + +verify.importFixAtPosition([ +`/* + * I'm a license or something + */ +import { f1 } from "ambient-module"; + +f1();` +]); \ No newline at end of file diff --git a/tests/cases/fourslash/importNameCodeFixNewImportAmbient3.ts b/tests/cases/fourslash/importNameCodeFixNewImportAmbient3.ts new file mode 100644 index 0000000000000..648293cce2ef0 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFixNewImportAmbient3.ts @@ -0,0 +1,30 @@ +/// + +//// let a = "I am a non-trivial statement that appears before imports"; +//// import d from "other-ambient-module" +//// [|import * as ns from "yet-another-ambient-module" +//// var x = v1/*0*/ + 5;|] + +// @Filename: ambientModule.ts +//// declare module "ambient-module" { +//// export function f1(); +//// export var v1; +//// } + +// @Filename: otherAmbientModule.ts +//// declare module "other-ambient-module" { +//// export default function f2(); +//// } + +// @Filename: yetAnotherAmbientModule.ts +//// declare module "yet-another-ambient-module" { +//// export function f3(); +//// export var v3; +//// } + +// test cases when there are no semicolons at the line end +verify.importFixAtPosition([ +`import * as ns from "yet-another-ambient-module" +import { v1 } from "ambient-module"; +var x = v1 + 5;` +]); \ No newline at end of file diff --git a/tests/cases/fourslash/importNameCodeFixNewImportBaseUrl0.ts b/tests/cases/fourslash/importNameCodeFixNewImportBaseUrl0.ts new file mode 100644 index 0000000000000..e15c2cf4399d8 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFixNewImportBaseUrl0.ts @@ -0,0 +1,19 @@ +/// + +//// [|f1/*0*/();|] + +// @Filename: tsconfig.json +//// { +//// "compilerOptions": { +//// "baseUrl": "./a" +//// } +//// } + +// @Filename: a/b.ts +//// export function f1() { }; + +verify.importFixAtPosition([ +`import { f1 } from "b"; + +f1();` +]); \ No newline at end of file diff --git a/tests/cases/fourslash/importNameCodeFixNewImportDefault0.ts b/tests/cases/fourslash/importNameCodeFixNewImportDefault0.ts new file mode 100644 index 0000000000000..3efe023e922b9 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFixNewImportDefault0.ts @@ -0,0 +1,12 @@ +/// + +//// [|f1/*0*/();|] + +// @Filename: module.ts +//// export default function f1() { }; + +verify.importFixAtPosition([ +`import f1 from "./module"; + +f1();` +]); \ No newline at end of file diff --git a/tests/cases/fourslash/importNameCodeFixNewImportFile0.ts b/tests/cases/fourslash/importNameCodeFixNewImportFile0.ts new file mode 100644 index 0000000000000..2372110437a22 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFixNewImportFile0.ts @@ -0,0 +1,13 @@ +/// + +//// [|f1/*0*/();|] + +// @Filename: module.ts +//// export function f1() {} +//// export var v1 = 5; + +verify.importFixAtPosition([ +`import { f1 } from "./module"; + +f1();` +]); \ No newline at end of file diff --git a/tests/cases/fourslash/importNameCodeFixNewImportFile1.ts b/tests/cases/fourslash/importNameCodeFixNewImportFile1.ts new file mode 100644 index 0000000000000..0223d96e0186c --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFixNewImportFile1.ts @@ -0,0 +1,18 @@ +/// + +//// [|/// +//// f1/*0*/();|] + +// @Filename: module.ts +//// export function f1() {} +//// export var v1 = 5; + +// @Filename: tripleSlashReference.ts +//// var x = 5;/*dummy*/ + +verify.importFixAtPosition([ +`/// +import { f1 } from "./module"; + +f1();` +]); \ No newline at end of file diff --git a/tests/cases/fourslash/importNameCodeFixNewImportFile2.ts b/tests/cases/fourslash/importNameCodeFixNewImportFile2.ts new file mode 100644 index 0000000000000..ca9330e9846a5 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFixNewImportFile2.ts @@ -0,0 +1,13 @@ +/// + +//// [|f1/*0*/();|] + +// @Filename: ../../other_dir/module.ts +//// export var v1 = 5; +//// export function f1(); + +verify.importFixAtPosition([ +`import { f1 } from "../../other_dir/module"; + +f1();` +]); \ No newline at end of file diff --git a/tests/cases/fourslash/importNameCodeFixNewImportNodeModules0.ts b/tests/cases/fourslash/importNameCodeFixNewImportNodeModules0.ts new file mode 100644 index 0000000000000..6013f865ddf15 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFixNewImportNodeModules0.ts @@ -0,0 +1,19 @@ +/// + +//// [|f1/*0*/();|] + +// @Filename: ../package.json +//// { "dependencies": { "fake-module": "latest" } } + +// @Filename: ../node_modules/fake-module/index.ts +//// export var v1 = 5; +//// export function f1(); + +// @Filename: ../node_modules/fake-module/package.json +//// {} + +verify.importFixAtPosition([ +`import { f1 } from "fake-module"; + +f1();` +]); diff --git a/tests/cases/fourslash/importNameCodeFixNewImportNodeModules1.ts b/tests/cases/fourslash/importNameCodeFixNewImportNodeModules1.ts new file mode 100644 index 0000000000000..6bffe41b27a04 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFixNewImportNodeModules1.ts @@ -0,0 +1,16 @@ +/// + +//// [|f1/*0*/();|] + +// @Filename: ../package.json +//// { "dependencies": { "fake-module": "latest" } } + +// @Filename: ../node_modules/fake-module/nested.ts +//// export var v1 = 5; +//// export function f1(); + +verify.importFixAtPosition([ +`import { f1 } from "fake-module/nested"; + +f1();` +]); \ No newline at end of file diff --git a/tests/cases/fourslash/importNameCodeFixNewImportNodeModules2.ts b/tests/cases/fourslash/importNameCodeFixNewImportNodeModules2.ts new file mode 100644 index 0000000000000..ff48fbe182cca --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFixNewImportNodeModules2.ts @@ -0,0 +1,25 @@ +/// + +//// [|f1/*0*/();|] + +// @Filename: ../package.json +//// { "dependencies": { "fake-module": "latest" } } + +// @Filename: ../node_modules/fake-module/notindex.d.ts +//// export var v1 = 5; +//// export function f1(); + +// @Filename: ../node_modules/fake-module/notindex.js +//// module.exports = { +//// v1: 5, +//// f1: function () {} +//// }; + +// @Filename: ../node_modules/fake-module/package.json +//// { "main":"./notindex.js", "typings":"./notindex.d.ts" } + +verify.importFixAtPosition([ +`import { f1 } from "fake-module"; + +f1();` +]); \ No newline at end of file diff --git a/tests/cases/fourslash/importNameCodeFixNewImportNodeModules3.ts b/tests/cases/fourslash/importNameCodeFixNewImportNodeModules3.ts new file mode 100644 index 0000000000000..b1143cb4b41c9 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFixNewImportNodeModules3.ts @@ -0,0 +1,14 @@ +/// + +// @Filename: /a.ts +//// [|f1/*0*/();|] + +// @Filename: /node_modules/@types/random/index.d.ts +//// export var v1 = 5; +//// export function f1(); + +verify.importFixAtPosition([ +`import { f1 } from "random"; + +f1();` +]); \ No newline at end of file diff --git a/tests/cases/fourslash/importNameCodeFixNewImportPaths0.ts b/tests/cases/fourslash/importNameCodeFixNewImportPaths0.ts new file mode 100644 index 0000000000000..93cd6f92ef595 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFixNewImportPaths0.ts @@ -0,0 +1,22 @@ +/// + +//// [|foo/*0*/();|] + +// @Filename: folder_a/f2.ts +//// export function foo() {}; + +// @Filename: tsconfig.json +//// { +//// "compilerOptions": { +//// "baseUrl": ".", +//// "paths": { +//// "a": [ "folder_a/f2" ] +//// } +//// } +//// } + +verify.importFixAtPosition([ +`import { foo } from "a"; + +foo();` +]); \ No newline at end of file diff --git a/tests/cases/fourslash/importNameCodeFixNewImportPaths1.ts b/tests/cases/fourslash/importNameCodeFixNewImportPaths1.ts new file mode 100644 index 0000000000000..bb0f1e6705a09 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFixNewImportPaths1.ts @@ -0,0 +1,22 @@ +/// + +//// [|foo/*0*/();|] + +// @Filename: folder_b/f2.ts +//// export function foo() {}; + +// @Filename: tsconfig.json +//// { +//// "compilerOptions": { +//// "baseUrl": ".", +//// "paths": { +//// "b/*": [ "folder_b/*" ] +//// } +//// } +//// } + +verify.importFixAtPosition([ +`import { foo } from "b/f2"; + +foo();` +]); \ No newline at end of file diff --git a/tests/cases/fourslash/importNameCodeFixNewImportRootDirs0.ts b/tests/cases/fourslash/importNameCodeFixNewImportRootDirs0.ts new file mode 100644 index 0000000000000..ae8ef03ccaccc --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFixNewImportRootDirs0.ts @@ -0,0 +1,23 @@ +/// + +// @Filename: a/f1.ts +//// [|foo/*0*/();|] + +// @Filename: b/c/f2.ts +//// export function foo() {}; + +// @Filename: tsconfig.json +//// { +//// "compilerOptions": { +//// "rootDirs": [ +//// "a", +//// "b/c" +//// ] +//// } +//// } + +verify.importFixAtPosition([ +`import { foo } from "./f2"; + +foo();` +]); \ No newline at end of file diff --git a/tests/cases/fourslash/importNameCodeFixNewImportTypeRoots0.ts b/tests/cases/fourslash/importNameCodeFixNewImportTypeRoots0.ts new file mode 100644 index 0000000000000..a6eb6a9075998 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFixNewImportTypeRoots0.ts @@ -0,0 +1,22 @@ +/// + +// @Filename: a/f1.ts +//// [|foo/*0*/();|] + +// @Filename: types/random/index.ts +//// export function foo() {}; + +// @Filename: tsconfig.json +//// { +//// "compilerOptions": { +//// "typeRoots": [ +//// "./types" +//// ] +//// } +//// } + +verify.importFixAtPosition([ +`import { foo } from "random"; + +foo();` +]); \ No newline at end of file diff --git a/tests/cases/fourslash/importNameCodeFixOptionalImport0.ts b/tests/cases/fourslash/importNameCodeFixOptionalImport0.ts new file mode 100644 index 0000000000000..30b482a94d792 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFixOptionalImport0.ts @@ -0,0 +1,20 @@ +/// + +// @Filename: a/f1.ts +//// [|import * as ns from "./foo"; +//// foo/*0*/();|] + +// @Filename: a/foo/bar.ts +//// export function foo() {}; + +// @Filename: a/foo.ts +//// export { foo } from "./foo/bar"; + +verify.importFixAtPosition([ +`import * as ns from "./foo"; +import { foo } from "./foo"; +foo();`, + +`import * as ns from "./foo"; +ns.foo();`, +]); \ No newline at end of file diff --git a/tests/cases/fourslash/importNameCodeFixOptionalImport1.ts b/tests/cases/fourslash/importNameCodeFixOptionalImport1.ts new file mode 100644 index 0000000000000..343b0692260c6 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFixOptionalImport1.ts @@ -0,0 +1,20 @@ +/// + +// @Filename: a/f1.ts +//// [|foo/*0*/();|] + +// @Filename: a/node_modules/bar/index.ts +//// export function foo() {}; + +// @Filename: a/foo.ts +//// export { foo } from "bar"; + +verify.importFixAtPosition([ +`import { foo } from "./foo"; + +foo();`, + +`import { foo } from "bar"; + +foo();`, +]); \ No newline at end of file diff --git a/tests/cases/fourslash/server/codefix.ts b/tests/cases/fourslash/server/codefix.ts index 7fbe2cb4fd7bd..9d0f42c9e4c6d 100644 --- a/tests/cases/fourslash/server/codefix.ts +++ b/tests/cases/fourslash/server/codefix.ts @@ -1,4 +1,4 @@ -/// +/// ////class Base{ ////}