diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 08152017942..1a6de4149ad 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -930,7 +930,8 @@ namespace ts { } } - function getOptionFromName(optionName: string, allowShort = false): CommandLineOption | undefined { + /** @internal */ + export function getOptionFromName(optionName: string, allowShort = false): CommandLineOption | undefined { optionName = optionName.toLowerCase(); const { optionNameMap, shortOptionNames } = getOptionNameMap(); // Try to translate short option names to their full equivalents. diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 01ca0bea4c7..7710e8c2d75 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -2592,6 +2592,22 @@ namespace ts { return startsWith(str, prefix) ? str.substr(prefix.length) : str; } + export function tryRemovePrefix(str: string, prefix: string): string | undefined { + return startsWith(str, prefix) ? str.substring(prefix.length) : undefined; + } + + export function tryRemoveDirectoryPrefix(path: string, dirPath: string): string | undefined { + const a = tryRemovePrefix(path, dirPath); + if (a === undefined) return undefined; + switch (a.charCodeAt(0)) { + case CharacterCodes.slash: + case CharacterCodes.backslash: + return a.slice(1); + default: + return undefined; + } + } + export function endsWith(str: string, suffix: string): boolean { const expectedPos = str.length - suffix.length; return expectedPos >= 0 && str.indexOf(suffix, expectedPos) === expectedPos; @@ -2601,6 +2617,10 @@ namespace ts { return endsWith(str, suffix) ? str.slice(0, str.length - suffix.length) : str; } + export function tryRemoveSuffix(str: string, suffix: string): string | undefined { + return endsWith(str, suffix) ? str.slice(0, str.length - suffix.length) : undefined; + } + export function stringContains(str: string, substring: string): boolean { return str.indexOf(substring) !== -1; } @@ -2801,6 +2821,7 @@ namespace ts { basePaths: ReadonlyArray; } + /** @param path directory of the tsconfig.json */ export function getFileMatcherPatterns(path: string, excludes: ReadonlyArray | undefined, includes: ReadonlyArray | undefined, useCaseSensitiveFileNames: boolean, currentDirectory: string): FileMatcherPatterns { path = normalizePath(path); currentDirectory = normalizePath(currentDirectory); @@ -2815,16 +2836,20 @@ namespace ts { }; } + export function getRegexFromPattern(pattern: string, useCaseSensitiveFileNames: boolean): RegExp { + return new RegExp(pattern, useCaseSensitiveFileNames ? "" : "i"); + } + + /** @param path directory of the tsconfig.json */ export function matchFiles(path: string, extensions: ReadonlyArray | undefined, excludes: ReadonlyArray | undefined, includes: ReadonlyArray | undefined, useCaseSensitiveFileNames: boolean, currentDirectory: string, depth: number | undefined, getFileSystemEntries: (path: string) => FileSystemEntries): string[] { path = normalizePath(path); currentDirectory = normalizePath(currentDirectory); const patterns = getFileMatcherPatterns(path, excludes, includes, useCaseSensitiveFileNames, currentDirectory); - const regexFlag = useCaseSensitiveFileNames ? "" : "i"; - const includeFileRegexes = patterns.includeFilePatterns && patterns.includeFilePatterns.map(pattern => new RegExp(pattern, regexFlag)); - const includeDirectoryRegex = patterns.includeDirectoryPattern && new RegExp(patterns.includeDirectoryPattern, regexFlag); - const excludeRegex = patterns.excludePattern && new RegExp(patterns.excludePattern, regexFlag); + const includeFileRegexes = patterns.includeFilePatterns && patterns.includeFilePatterns.map(pattern => getRegexFromPattern(pattern, useCaseSensitiveFileNames)); + const includeDirectoryRegex = patterns.includeDirectoryPattern && getRegexFromPattern(patterns.includeDirectoryPattern, useCaseSensitiveFileNames); + const excludeRegex = patterns.excludePattern && getRegexFromPattern(patterns.excludePattern, useCaseSensitiveFileNames); // Associate an array of results with each include regex. This keeps results in order of the "include" order. // If there are no "includes", then just put everything in results[0]. diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 8b4e12dc222..ff9c4d52c21 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -1088,7 +1088,7 @@ namespace ts { } export function getPropertyAssignment(objectLiteral: ObjectLiteralExpression, key: string, key2?: string): ReadonlyArray { - return filter(objectLiteral.properties, (property): property is PropertyAssignment => { + return objectLiteral.properties.filter((property): property is PropertyAssignment => { if (property.kind === SyntaxKind.PropertyAssignment) { const propName = getTextOfPropertyName(property.name); return key === propName || (!!key2 && key2 === propName); @@ -1105,12 +1105,15 @@ namespace ts { } export function getTsConfigPropArrayElementValue(tsConfigSourceFile: TsConfigSourceFile | undefined, propKey: string, elementValue: string): StringLiteral | undefined { + return firstDefined(getTsConfigPropArray(tsConfigSourceFile, propKey), property => + isArrayLiteralExpression(property.initializer) ? + find(property.initializer.elements, (element): element is StringLiteral => isStringLiteral(element) && element.text === elementValue) : + undefined); + } + + export function getTsConfigPropArray(tsConfigSourceFile: TsConfigSourceFile | undefined, propKey: string): ReadonlyArray { const jsonObjectLiteral = getTsConfigObjectLiteralExpression(tsConfigSourceFile); - return jsonObjectLiteral && - firstDefined(getPropertyAssignment(jsonObjectLiteral, propKey), property => - isArrayLiteralExpression(property.initializer) ? - find(property.initializer.elements, (element): element is StringLiteral => isStringLiteral(element) && element.text === elementValue) : - undefined); + return jsonObjectLiteral ? getPropertyAssignment(jsonObjectLiteral, propKey) : emptyArray; } export function getContainingFunction(node: Node): SignatureDeclaration | undefined { diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index f6214d4c719..fc8852107f4 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -3131,8 +3131,12 @@ Actual: ${stringify(fullActual)}`); assert(action.name === "Move to a new file" && action.description === "Move to a new file"); const editInfo = this.languageService.getEditsForRefactor(this.activeFile.fileName, this.formatCodeSettings, range, refactor.name, action.name, options.preferences || ts.defaultPreferences)!; - for (const edit of editInfo.edits) { - const newContent = options.newFileContents[edit.fileName]; + this.testNewFileContents(editInfo.edits, options.newFileContents); + } + + private testNewFileContents(edits: ReadonlyArray, newFileContents: { [fileName: string]: string }): void { + for (const edit of edits) { + const newContent = newFileContents[edit.fileName]; if (newContent === undefined) { this.raiseError(`There was an edit in ${edit.fileName} but new content was not specified.`); } @@ -3149,8 +3153,8 @@ Actual: ${stringify(fullActual)}`); } } - for (const fileName in options.newFileContents) { - assert(editInfo.edits.some(e => e.fileName === fileName)); + for (const fileName in newFileContents) { + assert(edits.some(e => e.fileName === fileName)); } } @@ -3360,12 +3364,8 @@ Actual: ${stringify(fullActual)}`); } public getEditsForFileRename(options: FourSlashInterface.GetEditsForFileRenameOptions): void { - const changes = this.languageService.getEditsForFileRename(options.oldPath, options.newPath, this.formatCodeSettings); - this.applyChanges(changes); - for (const fileName in options.newFileContents) { - this.openFile(fileName); - this.verifyCurrentFileContent(options.newFileContents[fileName]); - } + const changes = this.languageService.getEditsForFileRename(options.oldPath, options.newPath, this.formatCodeSettings, ts.defaultPreferences); + this.testNewFileContents(changes, options.newFileContents); } private getApplicableRefactors(positionOrRange: number | ts.TextRange, preferences = ts.defaultPreferences): ReadonlyArray { diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 4518a740a85..aa1f4143fc8 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -8400,15 +8400,23 @@ new C();` path: "/user.ts", content: 'import { x } from "./old";', }; + const newTs: File = { + path: "/new.ts", + content: "export const x = 0;", + }; + const tsconfig: File = { + path: "/tsconfig.json", + content: "{}", + }; - const host = createServerHost([userTs]); + const host = createServerHost([userTs, newTs, tsconfig]); const projectService = createProjectService(host); projectService.openClientFile(userTs.path); - const project = first(projectService.inferredProjects); + const project = projectService.configuredProjects.get(tsconfig.path)!; Debug.assert(!!project.resolveModuleNames); - const edits = project.getLanguageService().getEditsForFileRename("/old.ts", "/new.ts", testFormatOptions); + const edits = project.getLanguageService().getEditsForFileRename("/old.ts", "/new.ts", testFormatOptions, defaultPreferences); assert.deepEqual>(edits, [{ fileName: "/user.ts", textChanges: [{ diff --git a/src/server/session.ts b/src/server/session.ts index 191982a7cd4..f56b396814b 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1692,7 +1692,7 @@ namespace ts.server { private getEditsForFileRename(args: protocol.GetEditsForFileRenameRequestArgs, simplifiedResult: boolean): ReadonlyArray | ReadonlyArray { const { file, project } = this.getFileAndProject(args); - const changes = project.getLanguageService().getEditsForFileRename(args.oldFilePath, args.newFilePath, this.getFormatOptions(file)); + const changes = project.getLanguageService().getEditsForFileRename(args.oldFilePath, args.newFilePath, this.getFormatOptions(file), this.getPreferences(file)); return simplifiedResult ? this.mapTextChangesToCodeEdits(project, changes) : changes; } diff --git a/src/services/codefixes/moduleSpecifiers.ts b/src/services/codefixes/moduleSpecifiers.ts index 5a08f7d3dea..be92b917273 100644 --- a/src/services/codefixes/moduleSpecifiers.ts +++ b/src/services/codefixes/moduleSpecifiers.ts @@ -1,6 +1,14 @@ // Used by importFixes to synthesize import module specifiers. /* @internal */ namespace ts.moduleSpecifiers { + // Note: fromSourceFile is just for usesJsExtensionOnImports + export function getModuleSpecifier(program: Program, fromSourceFile: SourceFile, fromSourceFileName: string, toFileName: string, host: LanguageServiceHost, preferences: UserPreferences) { + const info = getInfo(program.getCompilerOptions(), fromSourceFile, fromSourceFileName, host); + const compilerOptions = program.getCompilerOptions(); + return getGlobalModuleSpecifier(toFileName, info, host, compilerOptions) || + first(getLocalModuleSpecifiers(toFileName, info, compilerOptions, preferences)); + } + // For each symlink/original for a module, returns a list of ways to import that file. export function getModuleSpecifiers( moduleSymbol: Symbol, @@ -9,84 +17,110 @@ namespace ts.moduleSpecifiers { host: LanguageServiceHost, preferences: UserPreferences, ): ReadonlyArray> { - const compilerOptions = program.getCompilerOptions(); - const { baseUrl, paths, rootDirs } = compilerOptions; - const moduleResolutionKind = getEmitModuleResolutionKind(compilerOptions); - const addJsExtension = usesJsExtensionOnImports(importingSourceFile); - const getCanonicalFileName = hostGetCanonicalFileName(host); - const sourceDirectory = getDirectoryPath(importingSourceFile.fileName); - const ambient = tryGetModuleNameFromAmbientModule(moduleSymbol); if (ambient) return [[ambient]]; + const compilerOptions = program.getCompilerOptions(); + const info = getInfo(compilerOptions, importingSourceFile, importingSourceFile.fileName, host); const modulePaths = getAllModulePaths(program, moduleSymbol.valueDeclaration.getSourceFile()); - const global = mapDefined(modulePaths, moduleFileName => - tryGetModuleNameFromTypeRoots(compilerOptions, host, getCanonicalFileName, moduleFileName, addJsExtension) || - tryGetModuleNameAsNodeModule(compilerOptions, moduleFileName, host, getCanonicalFileName, sourceDirectory) || - rootDirs && tryGetModuleNameFromRootDirs(rootDirs, moduleFileName, sourceDirectory, getCanonicalFileName)); - if (global.length) return global.map(g => [g]); + const global = mapDefined(modulePaths, moduleFileName => getGlobalModuleSpecifier(moduleFileName, info, host, compilerOptions)); + return global.length ? global.map(g => [g]) : modulePaths.map(moduleFileName => + getLocalModuleSpecifiers(moduleFileName, info, compilerOptions, preferences)); + } - return modulePaths.map(moduleFileName => { - const relativePath = removeExtensionAndIndexPostFix(ensurePathIsNonModuleName(getRelativePathFromDirectory(sourceDirectory, moduleFileName, getCanonicalFileName)), moduleResolutionKind, addJsExtension); - if (!baseUrl || preferences.importModuleSpecifierPreference === "relative") { - return [relativePath]; + interface Info { + readonly moduleResolutionKind: ModuleResolutionKind; + readonly addJsExtension: boolean; + readonly getCanonicalFileName: GetCanonicalFileName; + readonly sourceDirectory: string; + } + // importingSourceFileName is separate because getEditsForFileRename may need to specify an updated path + function getInfo(compilerOptions: CompilerOptions, importingSourceFile: SourceFile, importingSourceFileName: string, host: LanguageServiceHost): Info { + const moduleResolutionKind = getEmitModuleResolutionKind(compilerOptions); + const addJsExtension = usesJsExtensionOnImports(importingSourceFile); + const getCanonicalFileName = hostGetCanonicalFileName(host); + const sourceDirectory = getDirectoryPath(importingSourceFileName); + return { moduleResolutionKind, addJsExtension, getCanonicalFileName, sourceDirectory }; + } + + function getGlobalModuleSpecifier( + moduleFileName: string, + { addJsExtension, getCanonicalFileName, sourceDirectory }: Info, + host: LanguageServiceHost, + compilerOptions: CompilerOptions, + ) { + return tryGetModuleNameFromTypeRoots(compilerOptions, host, getCanonicalFileName, moduleFileName, addJsExtension) + || tryGetModuleNameAsNodeModule(compilerOptions, moduleFileName, host, getCanonicalFileName, sourceDirectory) + || compilerOptions.rootDirs && tryGetModuleNameFromRootDirs(compilerOptions.rootDirs, moduleFileName, sourceDirectory, getCanonicalFileName); + } + + function getLocalModuleSpecifiers( + moduleFileName: string, + { moduleResolutionKind, addJsExtension, getCanonicalFileName, sourceDirectory }: Info, + compilerOptions: CompilerOptions, + preferences: UserPreferences, + ) { + const { baseUrl, paths } = compilerOptions; + + const relativePath = removeExtensionAndIndexPostFix(ensurePathIsNonModuleName(getRelativePathFromDirectory(sourceDirectory, moduleFileName, getCanonicalFileName)), moduleResolutionKind, addJsExtension); + if (!baseUrl || preferences.importModuleSpecifierPreference === "relative") { + return [relativePath]; + } + + const relativeToBaseUrl = getRelativePathIfInDirectory(moduleFileName, baseUrl, getCanonicalFileName); + if (!relativeToBaseUrl) { + return [relativePath]; + } + + const importRelativeToBaseUrl = removeExtensionAndIndexPostFix(relativeToBaseUrl, moduleResolutionKind, addJsExtension); + if (paths) { + const fromPaths = tryGetModuleNameFromPaths(removeFileExtension(relativeToBaseUrl), importRelativeToBaseUrl, paths); + if (fromPaths) { + return [fromPaths]; } + } - const relativeToBaseUrl = getRelativePathIfInDirectory(moduleFileName, baseUrl, getCanonicalFileName); - if (!relativeToBaseUrl) { - return [relativePath]; - } + if (preferences.importModuleSpecifierPreference === "non-relative") { + return [importRelativeToBaseUrl]; + } - const importRelativeToBaseUrl = removeExtensionAndIndexPostFix(relativeToBaseUrl, moduleResolutionKind, addJsExtension); - if (paths) { - const fromPaths = tryGetModuleNameFromPaths(removeFileExtension(relativeToBaseUrl), importRelativeToBaseUrl, paths); - if (fromPaths) { - return [fromPaths]; - } - } + if (preferences.importModuleSpecifierPreference !== undefined) Debug.assertNever(preferences.importModuleSpecifierPreference); - if (preferences.importModuleSpecifierPreference === "non-relative") { - return [importRelativeToBaseUrl]; - } + if (isPathRelativeToParent(relativeToBaseUrl)) { + return [relativePath]; + } - if (preferences.importModuleSpecifierPreference !== undefined) Debug.assertNever(preferences.importModuleSpecifierPreference); + /* + Prefer a relative import over a baseUrl import if it doesn't traverse up to baseUrl. - if (isPathRelativeToParent(relativeToBaseUrl)) { - return [relativePath]; - } + Suppose we have: + baseUrl = /base + sourceDirectory = /base/a/b + moduleFileName = /base/foo/bar + Then: + relativePath = ../../foo/bar + getRelativePathNParents(relativePath) = 2 + pathFromSourceToBaseUrl = ../../ + getRelativePathNParents(pathFromSourceToBaseUrl) = 2 + 2 < 2 = false + In this case we should prefer using the baseUrl path "/a/b" instead of the relative path "../../foo/bar". - /* - Prefer a relative import over a baseUrl import if it doesn't traverse up to baseUrl. - - Suppose we have: - baseUrl = /base - sourceDirectory = /base/a/b - moduleFileName = /base/foo/bar - Then: - relativePath = ../../foo/bar - getRelativePathNParents(relativePath) = 2 - pathFromSourceToBaseUrl = ../../ - getRelativePathNParents(pathFromSourceToBaseUrl) = 2 - 2 < 2 = false - In this case we should prefer using the baseUrl path "/a/b" instead of the relative path "../../foo/bar". - - Suppose we have: - baseUrl = /base - sourceDirectory = /base/foo/a - moduleFileName = /base/foo/bar - Then: - relativePath = ../a - getRelativePathNParents(relativePath) = 1 - pathFromSourceToBaseUrl = ../../ - getRelativePathNParents(pathFromSourceToBaseUrl) = 2 - 1 < 2 = true - In this case we should prefer using the relative path "../a" instead of the baseUrl path "foo/a". - */ - const pathFromSourceToBaseUrl = ensurePathIsNonModuleName(getRelativePathFromDirectory(sourceDirectory, baseUrl, getCanonicalFileName)); - const relativeFirst = getRelativePathNParents(relativePath) < getRelativePathNParents(pathFromSourceToBaseUrl); - return relativeFirst ? [relativePath, importRelativeToBaseUrl] : [importRelativeToBaseUrl, relativePath]; - }); + Suppose we have: + baseUrl = /base + sourceDirectory = /base/foo/a + moduleFileName = /base/foo/bar + Then: + relativePath = ../a + getRelativePathNParents(relativePath) = 1 + pathFromSourceToBaseUrl = ../../ + getRelativePathNParents(pathFromSourceToBaseUrl) = 2 + 1 < 2 = true + In this case we should prefer using the relative path "../a" instead of the baseUrl path "foo/a". + */ + const pathFromSourceToBaseUrl = ensurePathIsNonModuleName(getRelativePathFromDirectory(sourceDirectory, baseUrl, getCanonicalFileName)); + const relativeFirst = getRelativePathNParents(relativePath) < getRelativePathNParents(pathFromSourceToBaseUrl); + return relativeFirst ? [relativePath, importRelativeToBaseUrl] : [importRelativeToBaseUrl, relativePath]; } function usesJsExtensionOnImports({ imports }: SourceFile): boolean { diff --git a/src/services/getEditsForFileRename.ts b/src/services/getEditsForFileRename.ts index 1726f5e0681..4bac4a2516d 100644 --- a/src/services/getEditsForFileRename.ts +++ b/src/services/getEditsForFileRename.ts @@ -1,68 +1,189 @@ /* @internal */ namespace ts { - export function getEditsForFileRename(program: Program, oldFilePath: string, newFilePath: string, host: LanguageServiceHost, formatContext: formatting.FormatContext): ReadonlyArray { - const pathUpdater = getPathUpdater(oldFilePath, newFilePath, host); + export function getEditsForFileRename(program: Program, oldFileOrDirPath: string, newFileOrDirPath: string, host: LanguageServiceHost, formatContext: formatting.FormatContext, preferences: UserPreferences): ReadonlyArray { + const useCaseSensitiveFileNames = hostUsesCaseSensitiveFileNames(host); + const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); + const oldToNew = getPathUpdater(oldFileOrDirPath, newFileOrDirPath, getCanonicalFileName); + const newToOld = getPathUpdater(newFileOrDirPath, oldFileOrDirPath, getCanonicalFileName); return textChanges.ChangeTracker.with({ host, formatContext }, changeTracker => { - updateTsconfigFiles(program, changeTracker, oldFilePath, newFilePath); - for (const { sourceFile, toUpdate } of getImportsToUpdate(program, oldFilePath, host)) { - const newPath = pathUpdater(isRef(toUpdate) ? toUpdate.fileName : toUpdate.text); - if (newPath !== undefined) { - const range = isRef(toUpdate) ? toUpdate : createStringRange(toUpdate, sourceFile); - changeTracker.replaceRangeWithText(sourceFile, range, isRef(toUpdate) ? newPath : removeFileExtension(newPath)); - } - } + updateTsconfigFiles(program, changeTracker, oldToNew, newFileOrDirPath, host.getCurrentDirectory(), useCaseSensitiveFileNames); + updateImports(program, changeTracker, oldToNew, newToOld, host, getCanonicalFileName, preferences); }); } - function updateTsconfigFiles(program: Program, changeTracker: textChanges.ChangeTracker, oldFilePath: string, newFilePath: string): void { - const configFile = program.getCompilerOptions().configFile; - if (!configFile) return; - const oldFile = getTsConfigPropArrayElementValue(configFile, "files", oldFilePath); - if (oldFile) { - changeTracker.replaceRangeWithText(configFile, createStringRange(oldFile, configFile), newFilePath); - } - } - - interface ToUpdate { - readonly sourceFile: SourceFile; - readonly toUpdate: StringLiteralLike | FileReference; - } - function isRef(toUpdate: StringLiteralLike | FileReference): toUpdate is FileReference { - return "fileName" in toUpdate; - } - - function getImportsToUpdate(program: Program, oldFilePath: string, host: LanguageServiceHost): ReadonlyArray { - const result: ToUpdate[] = []; - for (const sourceFile of program.getSourceFiles()) { - for (const ref of sourceFile.referencedFiles) { - if (resolveTripleslashReference(ref.fileName, sourceFile.fileName) === oldFilePath) { - result.push({ sourceFile, toUpdate: ref }); - } - } - - for (const importStringLiteral of sourceFile.imports) { - const resolved = host.resolveModuleNames - ? host.getResolvedModuleWithFailedLookupLocationsFromCache && host.getResolvedModuleWithFailedLookupLocationsFromCache(importStringLiteral.text, sourceFile.fileName) - : program.getResolvedModuleWithFailedLookupLocationsFromCache(importStringLiteral.text, sourceFile.fileName); - // We may or may not have picked up on the file being renamed, so maybe successfully resolved to oldFilePath, or maybe that's in failedLookupLocations - if (resolved && contains(resolved.resolvedModule ? [resolved.resolvedModule.resolvedFileName] : resolved.failedLookupLocations, oldFilePath)) { - result.push({ sourceFile, toUpdate: importStringLiteral }); - } - } - } - return result; - } - - function getPathUpdater(oldFilePath: string, newFilePath: string, host: LanguageServiceHost): (oldPath: string) => string | undefined { - // Get the relative path from old to new location, and append it on to the end of imports and normalize. - const rel = getRelativePathFromFile(oldFilePath, newFilePath, createGetCanonicalFileName(hostUsesCaseSensitiveFileNames(host))); - return oldPath => { - if (!pathIsRelative(oldPath)) return; - return ensurePathIsNonModuleName(normalizePath(combinePaths(getDirectoryPath(oldPath), rel))); + /** If 'path' refers to an old directory, returns path in the new directory. */ + type PathUpdater = (path: string) => string | undefined; + function getPathUpdater(oldFileOrDirPath: string, newFileOrDirPath: string, getCanonicalFileName: GetCanonicalFileName): PathUpdater { + const canonicalOldPath = getCanonicalFileName(oldFileOrDirPath); + return path => { + const canonicalPath = getCanonicalFileName(path); + if (canonicalPath === canonicalOldPath) return newFileOrDirPath; + const suffix = tryRemoveDirectoryPrefix(canonicalPath, canonicalOldPath); + return suffix === undefined ? undefined : newFileOrDirPath + "/" + suffix; }; } + function updateTsconfigFiles(program: Program, changeTracker: textChanges.ChangeTracker, oldToNew: PathUpdater, newFileOrDirPath: string, currentDirectory: string, useCaseSensitiveFileNames: boolean): void { + const { configFile } = program.getCompilerOptions(); + if (!configFile) return; + const configDir = getDirectoryPath(configFile.fileName); + + const jsonObjectLiteral = getTsConfigObjectLiteralExpression(configFile); + if (!jsonObjectLiteral) return; + + forEachProperty(jsonObjectLiteral, (property, propertyName) => { + switch (propertyName) { + case "files": + case "include": + case "exclude": { + const foundExactMatch = updatePaths(property); + if (!foundExactMatch && propertyName === "include" && isArrayLiteralExpression(property.initializer)) { + const includes = mapDefined(property.initializer.elements, e => isStringLiteral(e) ? e.text : undefined); + const matchers = getFileMatcherPatterns(configDir, /*excludes*/ [], includes, useCaseSensitiveFileNames, currentDirectory); + // If there isn't some include for this, add a new one. + if (!getRegexFromPattern(Debug.assertDefined(matchers.includeFilePattern), useCaseSensitiveFileNames).test(newFileOrDirPath)) { + changeTracker.insertNodeAfter(configFile, last(property.initializer.elements), createStringLiteral(relativePath(newFileOrDirPath))); + } + } + break; + } + case "compilerOptions": + forEachProperty(property.initializer, (property, propertyName) => { + const option = getOptionFromName(propertyName); + if (option && (option.isFilePath || option.type === "list" && (option as CommandLineOptionOfListType).element.isFilePath)) { + updatePaths(property); + } + else if (propertyName === "paths") { + forEachProperty(property.initializer, (pathsProperty) => { + if (!isArrayLiteralExpression(pathsProperty.initializer)) return; + for (const e of pathsProperty.initializer.elements) { + tryUpdateString(e); + } + }); + } + }); + break; + } + }); + + function updatePaths(property: PropertyAssignment): boolean { + // Type annotation needed due to #7294 + const elements: ReadonlyArray = isArrayLiteralExpression(property.initializer) ? property.initializer.elements : [property.initializer]; + let foundExactMatch = false; + for (const element of elements) { + foundExactMatch = tryUpdateString(element) || foundExactMatch; + } + return foundExactMatch; + } + + function tryUpdateString(element: Expression): boolean { + if (!isStringLiteral(element)) return false; + const elementFileName = combinePathsSafe(configDir, element.text); + + const updated = oldToNew(elementFileName); + if (updated !== undefined) { + changeTracker.replaceRangeWithText(configFile!, createStringRange(element, configFile!), relativePath(updated)); + return true; + } + return false; + } + + function relativePath(path: string): string { + return getRelativePathFromDirectory(configDir, path, /*ignoreCase*/ !useCaseSensitiveFileNames); + } + } + + function updateImports( + program: Program, + changeTracker: textChanges.ChangeTracker, + oldToNew: PathUpdater, + newToOld: PathUpdater, + host: LanguageServiceHost, + getCanonicalFileName: GetCanonicalFileName, + preferences: UserPreferences, + ): void { + for (const sourceFile of program.getSourceFiles()) { + const newImportFromPath = oldToNew(sourceFile.fileName) || sourceFile.fileName; + const newImportFromDirectory = getDirectoryPath(newImportFromPath); + + const oldFromNew: string | undefined = newToOld(sourceFile.fileName); + const oldImportFromPath: string = oldFromNew || sourceFile.fileName; + const oldImportFromDirectory = getDirectoryPath(oldImportFromPath); + + updateImportsWorker(sourceFile, changeTracker, + referenceText => { + if (!pathIsRelative(referenceText)) return undefined; + const oldAbsolute = combinePathsSafe(oldImportFromDirectory, referenceText); + const newAbsolute = oldToNew(oldAbsolute); + return newAbsolute === undefined ? undefined : ensurePathIsNonModuleName(getRelativePathFromDirectory(newImportFromDirectory, newAbsolute, getCanonicalFileName)); + }, + importLiteral => { + const toImport = oldFromNew !== undefined + // If we're at the new location (file was already renamed), need to redo module resolution starting from the old location. + // TODO:GH#18217 + ? getSourceFileToImportFromResolved(resolveModuleName(importLiteral.text, oldImportFromPath, program.getCompilerOptions(), host as ModuleResolutionHost), oldToNew, program) + : getSourceFileToImport(importLiteral, sourceFile, program, host, oldToNew); + return toImport === undefined ? undefined : moduleSpecifiers.getModuleSpecifier(program, sourceFile, newImportFromPath, toImport, host, preferences); + }); + } + } + + function combineNormal(pathA: string, pathB: string): string { + return normalizePath(combinePaths(pathA, pathB)); + } + function combinePathsSafe(pathA: string, pathB: string): string { + return ensurePathIsNonModuleName(combineNormal(pathA, pathB)); + } + + function getSourceFileToImport(importLiteral: StringLiteralLike, importingSourceFile: SourceFile, program: Program, host: LanguageServiceHost, oldToNew: PathUpdater): string | undefined { + const symbol = program.getTypeChecker().getSymbolAtLocation(importLiteral); + if (symbol) { + if (symbol.declarations.some(d => isAmbientModule(d))) return undefined; // No need to update if it's an ambient module + const oldFileName = find(symbol.declarations, isSourceFile)!.fileName; + return oldToNew(oldFileName) || oldFileName; + } + else { + const resolved = host.resolveModuleNames + ? host.getResolvedModuleWithFailedLookupLocationsFromCache && host.getResolvedModuleWithFailedLookupLocationsFromCache(importLiteral.text, importingSourceFile.fileName) + : program.getResolvedModuleWithFailedLookupLocationsFromCache(importLiteral.text, importingSourceFile.fileName); + return getSourceFileToImportFromResolved(resolved, oldToNew, program); + } + } + + function getSourceFileToImportFromResolved(resolved: ResolvedModuleWithFailedLookupLocations | undefined, oldToNew: PathUpdater, program: Program): string | undefined { + return resolved && ( + (resolved.resolvedModule && getIfInProgram(resolved.resolvedModule.resolvedFileName)) || firstDefined(resolved.failedLookupLocations, getIfInProgram)); + + function getIfInProgram(oldLocation: string): string | undefined { + const newLocation = oldToNew(oldLocation); + return program.getSourceFile(oldLocation) || newLocation !== undefined && program.getSourceFile(newLocation) + ? newLocation || oldLocation + : undefined; + } + } + + function updateImportsWorker(sourceFile: SourceFile, changeTracker: textChanges.ChangeTracker, updateRef: (refText: string) => string | undefined, updateImport: (importLiteral: StringLiteralLike) => string | undefined) { + for (const ref of sourceFile.referencedFiles) { + const updated = updateRef(ref.fileName); + if (updated !== undefined && updated !== sourceFile.text.slice(ref.pos, ref.end)) changeTracker.replaceRangeWithText(sourceFile, ref, updated); + } + + for (const importStringLiteral of sourceFile.imports) { + const updated = updateImport(importStringLiteral); + if (updated !== undefined && updated !== importStringLiteral.text) changeTracker.replaceRangeWithText(sourceFile, createStringRange(importStringLiteral, sourceFile), updated); + } + } + function createStringRange(node: StringLiteralLike, sourceFile: SourceFileLike): TextRange { return createTextRange(node.getStart(sourceFile) + 1, node.end - 1); } + + function forEachProperty(objectLiteral: Expression, cb: (property: PropertyAssignment, propertyName: string) => void) { + if (!isObjectLiteralExpression(objectLiteral)) return; + for (const property of objectLiteral.properties) { + if (isPropertyAssignment(property) && isStringLiteral(property.name)) { + cb(property, property.name.text); + } + } + } } diff --git a/src/services/services.ts b/src/services/services.ts index 467fe902025..ab86025d839 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1981,8 +1981,8 @@ namespace ts { return OrganizeImports.organizeImports(sourceFile, formatContext, host, program, preferences); } - function getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings): ReadonlyArray { - return ts.getEditsForFileRename(getProgram()!, oldFilePath, newFilePath, host, formatting.getFormatContext(formatOptions)); + function getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences = defaultPreferences): ReadonlyArray { + return ts.getEditsForFileRename(getProgram()!, oldFilePath, newFilePath, host, formatting.getFormatContext(formatOptions), preferences); } function applyCodeActionCommand(action: CodeActionCommand): Promise; diff --git a/src/services/textChanges.ts b/src/services/textChanges.ts index 528ec8268b2..02c29b0568d 100644 --- a/src/services/textChanges.ts +++ b/src/services/textChanges.ts @@ -496,7 +496,7 @@ namespace ts.textChanges { else if (isStatement(node) || isClassOrTypeElement(node)) { return { suffix: this.newLineCharacter }; } - else if (isVariableDeclaration(node)) { + else if (isVariableDeclaration(node) || isStringLiteral(node)) { return { prefix: ", " }; } else if (isPropertyAssignment(node)) { diff --git a/src/services/types.ts b/src/services/types.ts index 1f170d7e9ef..75dc45c3552 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -342,7 +342,7 @@ namespace ts { getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined): ApplicableRefactorInfo[]; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): ReadonlyArray; - getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings): ReadonlyArray; + getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): ReadonlyArray; getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput; diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index a6657b4d341..8f065e362d2 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -4572,7 +4572,7 @@ declare namespace ts { getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined): ApplicableRefactorInfo[]; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): ReadonlyArray; - getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings): ReadonlyArray; + getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): ReadonlyArray; getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput; getProgram(): Program | undefined; dispose(): void; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index d71bb5117ba..86c001cdec4 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -4572,7 +4572,7 @@ declare namespace ts { getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined): ApplicableRefactorInfo[]; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): ReadonlyArray; - getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings): ReadonlyArray; + getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): ReadonlyArray; getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput; getProgram(): Program | undefined; dispose(): void; diff --git a/tests/cases/fourslash/getEditsForFileRename.ts b/tests/cases/fourslash/getEditsForFileRename.ts index 8e53883b37e..03aab260978 100644 --- a/tests/cases/fourslash/getEditsForFileRename.ts +++ b/tests/cases/fourslash/getEditsForFileRename.ts @@ -14,8 +14,11 @@ /////// ////import old from "../old"; +// @Filename: /src/new.ts +//// + // @Filename: /tsconfig.json -////{ "files": ["/a.ts", "/src/a.ts", "/src/foo/a.ts", "/src/old.ts"] } +////{ "files": ["a.ts", "src/a.ts", "src/foo/a.ts", "src/old.ts"] } verify.getEditsForFileRename({ oldPath: "/src/old.ts", @@ -24,6 +27,6 @@ verify.getEditsForFileRename({ "/a.ts": '/// \nimport old from "./src/new";', "/src/a.ts": '/// \nimport old from "./new";', "/src/foo/a.ts": '/// \nimport old from "../new";', - "/tsconfig.json": '{ "files": ["/a.ts", "/src/a.ts", "/src/foo/a.ts", "/src/new.ts"] }', + "/tsconfig.json": '{ "files": ["a.ts", "src/a.ts", "src/foo/a.ts", "src/new.ts"] }', }, }); diff --git a/tests/cases/fourslash/getEditsForFileRename_amd.ts b/tests/cases/fourslash/getEditsForFileRename_amd.ts new file mode 100644 index 00000000000..22bf01e823c --- /dev/null +++ b/tests/cases/fourslash/getEditsForFileRename_amd.ts @@ -0,0 +1,18 @@ +/// + +// @moduleResolution: classic + +// @Filename: /src/user.ts +////import { x } from "old"; + +// @Filename: /src/old.ts +//// + +verify.getEditsForFileRename({ + oldPath: "/src/old.ts", + newPath: "/src/new.ts", + newFileContents: { + "/src/user.ts": +`import { x } from "./new";`, + }, +}); diff --git a/tests/cases/fourslash/getEditsForFileRename_caseInsensitive.ts b/tests/cases/fourslash/getEditsForFileRename_caseInsensitive.ts new file mode 100644 index 00000000000..506c93ad344 --- /dev/null +++ b/tests/cases/fourslash/getEditsForFileRename_caseInsensitive.ts @@ -0,0 +1,15 @@ +/// + +// @Filename: /a.ts +////export const a = 0; + +// @Filename: /b.ts +////import { a } from "./A"; + +verify.getEditsForFileRename({ + oldPath: "/a.ts", + newPath: "/eh.ts", + newFileContents: { + "/b.ts": 'import { a } from "./eh";', + }, +}); diff --git a/tests/cases/fourslash/getEditsForFileRename_directory.ts b/tests/cases/fourslash/getEditsForFileRename_directory.ts new file mode 100644 index 00000000000..8089d6fa138 --- /dev/null +++ b/tests/cases/fourslash/getEditsForFileRename_directory.ts @@ -0,0 +1,61 @@ +/// + +// @Filename: /a.ts +/////// +////import old from "./src/old"; +////import old2 from "./src/old/file"; +////export default 0; + +// @Filename: /src/b.ts +/////// +////import old from "./old"; +////import old2 from "./old/file"; +////export default 0; + +// @Filename: /src/foo/c.ts +/////// +////import old from "../old"; +////import old2 from "../old/file"; +////export default 0; + +// @Filename: /src/new/index.ts +////import a from "../../a"; +////import a2 from "../b"; +////import a3 from "../foo/c"; +////import f from "./file"; +////export default 0; + +// @Filename: /src/new/file.ts +//// + +// @Filename: /tsconfig.json +////{ "files": ["a.ts", "src/b.ts", "src/foo/c.ts", "src/old/index.ts", "src/old/file.ts"] } + +verify.getEditsForFileRename({ + oldPath: "/src/old", + newPath: "/src/new", + newFileContents: { + "/a.ts": +`/// +import old from "./src/new"; +import old2 from "./src/new/file"; +export default 0;`, + + "/src/b.ts": +`/// +import old from "./new"; +import old2 from "./new/file"; +export default 0;`, + + "/src/foo/c.ts": +`/// +import old from "../new"; +import old2 from "../new/file"; +export default 0;`, + + // No change to /src/new + + "/tsconfig.json": +`{ "files": ["a.ts", "src/b.ts", "src/foo/c.ts", "src/new/index.ts", "src/new/file.ts"] }`, + }, +}); diff --git a/tests/cases/fourslash/getEditsForFileRename_directory_down.ts b/tests/cases/fourslash/getEditsForFileRename_directory_down.ts new file mode 100644 index 00000000000..c4f42e3cc52 --- /dev/null +++ b/tests/cases/fourslash/getEditsForFileRename_directory_down.ts @@ -0,0 +1,66 @@ +/// + +// @Filename: /a.ts +/////// +////import old from "./src/old"; +////import old2 from "./src/old/file"; +////export default 0; + +// @Filename: /src/b.ts +/////// +////import old from "./old"; +////import old2 from "./old/file"; +////export default 0; + +// @Filename: /src/foo/c.ts +/////// +////import old from "../old"; +////import old2 from "../old/file"; +////export default 0; + +// @Filename: /src/newDir/new/index.ts +////import a from "../../a"; +////import a2 from "../b"; +////import a3 from "../foo/c"; +////import f from "./file"; +////export default 0; + +// @Filename: /src/newDir/new/file.ts +//// + +// @Filename: /tsconfig.json +////{ "files": ["a.ts", "src/b.ts", "src/foo/c.ts", "src/old/index.ts", "src/old/file.ts"] } + +verify.getEditsForFileRename({ + oldPath: "/src/old", + newPath: "/src/newDir/new", + newFileContents: { + "/a.ts": +`/// +import old from "./src/newDir/new"; +import old2 from "./src/newDir/new/file"; +export default 0;`, + + "/src/b.ts": +`/// +import old from "./newDir/new"; +import old2 from "./newDir/new/file"; +export default 0;`, + + "/src/foo/c.ts": +`/// +import old from "../newDir/new"; +import old2 from "../newDir/new/file"; +export default 0;`, + + "/src/newDir/new/index.ts": +`import a from "../../../a"; +import a2 from "../../b"; +import a3 from "../../foo/c"; +import f from "./file"; +export default 0;`, + + "/tsconfig.json": +`{ "files": ["a.ts", "src/b.ts", "src/foo/c.ts", "src/newDir/new/index.ts", "src/newDir/new/file.ts"] }`, + }, +}); diff --git a/tests/cases/fourslash/getEditsForFileRename_directory_noUpdateNodeModulesImport.ts b/tests/cases/fourslash/getEditsForFileRename_directory_noUpdateNodeModulesImport.ts new file mode 100644 index 00000000000..c2674cc1670 --- /dev/null +++ b/tests/cases/fourslash/getEditsForFileRename_directory_noUpdateNodeModulesImport.ts @@ -0,0 +1,13 @@ +/// + +// @Filename: /a/b/file1.ts +////import { foo } from "foo"; + +// @Filename: /a/b/node_modules/foo/index.d.ts +////export const foo = 0; + +verify.getEditsForFileRename({ + oldPath: "/a/b", + newPath: "/a/d", + newFileContents: {}, +}); diff --git a/tests/cases/fourslash/getEditsForFileRename_directory_up.ts b/tests/cases/fourslash/getEditsForFileRename_directory_up.ts new file mode 100644 index 00000000000..c63cfe888f1 --- /dev/null +++ b/tests/cases/fourslash/getEditsForFileRename_directory_up.ts @@ -0,0 +1,66 @@ +/// + +// @Filename: /a.ts +/////// +////import old from "./src/old"; +////import old2 from "./src/old/file"; +////export default 0; + +// @Filename: /src/b.ts +/////// +////import old from "./old"; +////import old2 from "./old/file"; +////export default 0; + +// @Filename: /src/foo/c.ts +/////// +////import old from "../old"; +////import old2 from "../old/file"; +////export default 0; + +// @Filename: /newDir/new/index.ts +////import a from "../../a"; +////import a2 from "../b"; +////import a3 from "../foo/c"; +////import f from "./file"; +////export default 0; + +// @Filename: /newDir/new/file.ts +//// + +// @Filename: /tsconfig.json +////{ "files": ["a.ts", "src/b.ts", "src/foo/c.ts", "src/old/index.ts", "src/old/file.ts"] } + +verify.getEditsForFileRename({ + oldPath: "/src/old", + newPath: "/newDir/new", + newFileContents: { + "/a.ts": +`/// +import old from "./newDir/new"; +import old2 from "./newDir/new/file"; +export default 0;`, + + "/src/b.ts": +`/// +import old from "../newDir/new"; +import old2 from "../newDir/new/file"; +export default 0;`, + + "/src/foo/c.ts": +`/// +import old from "../../newDir/new"; +import old2 from "../../newDir/new/file"; +export default 0;`, + + "/newDir/new/index.ts": +`import a from "../../a"; +import a2 from "../../src/b"; +import a3 from "../../src/foo/c"; +import f from "./file"; +export default 0;`, + + "/tsconfig.json": +`{ "files": ["a.ts", "src/b.ts", "src/foo/c.ts", "newDir/new/index.ts", "newDir/new/file.ts"] }`, + }, +}); diff --git a/tests/cases/fourslash/getEditsForFileRename_jsExtension.ts b/tests/cases/fourslash/getEditsForFileRename_jsExtension.ts new file mode 100644 index 00000000000..9c4e423a76a --- /dev/null +++ b/tests/cases/fourslash/getEditsForFileRename_jsExtension.ts @@ -0,0 +1,18 @@ +/// + +// @allowJs: true + +// @Filename: /src/a.js +////export const a = 0; + +// @Filename: /b.js +////import { a } from "./src/a.js"; + +verify.getEditsForFileRename({ + oldPath: "/b.js", + newPath: "/src/b.js", + newFileContents: { + "/b.js": +`import { a } from "./a.js";`, + }, +}); diff --git a/tests/cases/fourslash/getEditsForFileRename_oldFileStillPresent.ts b/tests/cases/fourslash/getEditsForFileRename_oldFileStillPresent.ts index c4ebf341a5e..34084d9d9df 100644 --- a/tests/cases/fourslash/getEditsForFileRename_oldFileStillPresent.ts +++ b/tests/cases/fourslash/getEditsForFileRename_oldFileStillPresent.ts @@ -18,7 +18,7 @@ ////import old from "../old"; // @Filename: /tsconfig.json -////{ "files": ["/a.ts", "/src/a.ts", "/src/foo/a.ts", "/src/old.ts"] } +////{ "files": ["a.ts", "src/a.ts", "src/foo/a.ts", "src/old.ts"] } verify.getEditsForFileRename({ oldPath: "/src/old.ts", @@ -27,6 +27,6 @@ verify.getEditsForFileRename({ "/a.ts": '/// \nimport old from "./src/new";', "/src/a.ts": '/// \nimport old from "./new";', "/src/foo/a.ts": '/// \nimport old from "../new";', - "/tsconfig.json": '{ "files": ["/a.ts", "/src/a.ts", "/src/foo/a.ts", "/src/new.ts"] }', + "/tsconfig.json": '{ "files": ["a.ts", "src/a.ts", "src/foo/a.ts", "src/new.ts"] }', }, }); diff --git a/tests/cases/fourslash/getEditsForFileRename_renameFromIndex.ts b/tests/cases/fourslash/getEditsForFileRename_renameFromIndex.ts new file mode 100644 index 00000000000..cddf212128c --- /dev/null +++ b/tests/cases/fourslash/getEditsForFileRename_renameFromIndex.ts @@ -0,0 +1,43 @@ +/// + +// @Filename: /a.ts +/////// +////import old from "./src"; +////import old2 from "./src/index"; + +// @Filename: /src/a.ts +/////// +////import old from "."; +////import old2 from "./index"; + +// @Filename: /src/foo/a.ts +/////// +////import old from ".."; +////import old2 from "../index"; + +// @Filename: /src/index.ts +//// + +// @Filename: /tsconfig.json +////{ "files": ["a.ts", "src/a.ts", "src/foo/a.ts", "src/index.ts"] } + +verify.getEditsForFileRename({ + oldPath: "/src/index.ts", + newPath: "/src/new.ts", + newFileContents: { + "/a.ts": +`/// +import old from "./src/new"; +import old2 from "./src/new";`, + "/src/a.ts": +`/// +import old from "./new"; +import old2 from "./new";`, + "/src/foo/a.ts": +`/// +import old from "../new"; +import old2 from "../new";`, + "/tsconfig.json": +'{ "files": ["a.ts", "src/a.ts", "src/foo/a.ts", "src/new.ts"] }', + }, +}); diff --git a/tests/cases/fourslash/getEditsForFileRename_renameToIndex.ts b/tests/cases/fourslash/getEditsForFileRename_renameToIndex.ts new file mode 100644 index 00000000000..a1a052ee557 --- /dev/null +++ b/tests/cases/fourslash/getEditsForFileRename_renameToIndex.ts @@ -0,0 +1,37 @@ +/// + +// @Filename: /a.ts +/////// +////import old from "./src/old"; + +// @Filename: /src/a.ts +/////// +////import old from "./old"; + +// @Filename: /src/foo/a.ts +/////// +////import old from "../old"; + +// @Filename: /src/old.ts +//// + +// @Filename: /tsconfig.json +////{ "files": ["a.ts", "src/a.ts", "src/foo/a.ts", "src/old.ts"] } + +verify.getEditsForFileRename({ + oldPath: "/src/old.ts", + newPath: "/src/index.ts", + newFileContents: { + "/a.ts": +`/// +import old from "./src";`, + "/src/a.ts": +`/// +import old from ".";`, + "/src/foo/a.ts": +`/// +import old from "..";`, + "/tsconfig.json": +'{ "files": ["a.ts", "src/a.ts", "src/foo/a.ts", "src/index.ts"] }', + }, +}); diff --git a/tests/cases/fourslash/getEditsForFileRename_shortenRelativePaths.ts b/tests/cases/fourslash/getEditsForFileRename_shortenRelativePaths.ts new file mode 100644 index 00000000000..d5a62f219d5 --- /dev/null +++ b/tests/cases/fourslash/getEditsForFileRename_shortenRelativePaths.ts @@ -0,0 +1,16 @@ +/// + +// @Filename: /src/foo/x.ts +//// + +// @Filename: /src/foo/new.ts +////import { x } from "./foo/x"; + +verify.getEditsForFileRename({ + oldPath: "/src/old.ts", + newPath: "/src/foo/new.ts", + newFileContents: { + "/src/foo/new.ts": +`import { x } from "./x";`, + }, +}); diff --git a/tests/cases/fourslash/getEditsForFileRename_subDir.ts b/tests/cases/fourslash/getEditsForFileRename_subDir.ts new file mode 100644 index 00000000000..ae75f190b9d --- /dev/null +++ b/tests/cases/fourslash/getEditsForFileRename_subDir.ts @@ -0,0 +1,16 @@ +/// + +// @Filename: /src/foo/a.ts +//// + +// @Filename: /src/dir/new.ts +////import a from "./foo/a"; + +verify.getEditsForFileRename({ + oldPath: "/src/old.ts", + newPath: "/src/dir/new.ts", + newFileContents: { + "/src/dir/new.ts": +`import a from "../foo/a";`, + }, +}); diff --git a/tests/cases/fourslash/getEditsForFileRename_tsconfig.ts b/tests/cases/fourslash/getEditsForFileRename_tsconfig.ts new file mode 100644 index 00000000000..611bff18539 --- /dev/null +++ b/tests/cases/fourslash/getEditsForFileRename_tsconfig.ts @@ -0,0 +1,41 @@ +/// + +// @Filename: /src/tsconfig.json +////{ +//// "compilerOptions": { +//// "baseUrl": "./old", +//// "mapRoot": "../src/old", +//// "paths": { +//// "foo": ["old"], +//// }, +//// "rootDir": "old", +//// "rootDirs": ["old"], +//// "typeRoots": ["old"], +//// }, +//// "files": ["old/a.ts"], +//// "include": ["old/*.ts"], +//// "exclude": ["old"], +////} + +verify.getEditsForFileRename({ + oldPath: "/src/old", + newPath: "/src/new", + newFileContents: { + "/src/tsconfig.json": +`{ + "compilerOptions": { + "baseUrl": "new", + "mapRoot": "new", + "paths": { + "foo": ["new"], + }, + "rootDir": "new", + "rootDirs": ["new"], + "typeRoots": ["new"], + }, + "files": ["new/a.ts"], + "include": ["new/*.ts"], + "exclude": ["new"], +}`, + }, +}); diff --git a/tests/cases/fourslash/getEditsForFileRename_tsconfig_include_add.ts b/tests/cases/fourslash/getEditsForFileRename_tsconfig_include_add.ts new file mode 100644 index 00000000000..5b1ef6791c0 --- /dev/null +++ b/tests/cases/fourslash/getEditsForFileRename_tsconfig_include_add.ts @@ -0,0 +1,17 @@ +/// + +// @Filename: /src/tsconfig.json +////{ +//// "include": ["dir"], +////} + +verify.getEditsForFileRename({ + oldPath: "/src/dir/a.ts", + newPath: "/src/newDir/b.ts", + newFileContents: { + "/src/tsconfig.json": +`{ + "include": ["dir", "newDir/b.ts"], +}`, + }, +}); diff --git a/tests/cases/fourslash/getEditsForFileRename_tsconfig_include_noChange.ts b/tests/cases/fourslash/getEditsForFileRename_tsconfig_include_noChange.ts new file mode 100644 index 00000000000..dc2200e17e8 --- /dev/null +++ b/tests/cases/fourslash/getEditsForFileRename_tsconfig_include_noChange.ts @@ -0,0 +1,12 @@ +/// + +// @Filename: /src/tsconfig.json +////{ +//// "include": ["dir"], +////} + +verify.getEditsForFileRename({ + oldPath: "/src/dir/a.ts", + newPath: "/src/dir/b.ts", + newFileContents: {}, +});