Skip to content

Commit

Permalink
getEditsForFileRename: Support directory rename (#24305) (#24568)
Browse files Browse the repository at this point in the history
* getEditsForFileRename: Support directory rename

* Code review

* Handle imports inside the new file/directory

* Document path updaters

* Shorten relative paths where possible

* Reduce duplicate code

* Rewrite, use moduleSpecifiers.ts to get module specifiers from scratch instead of updating relative paths

* Update additional tsconfig.json fields

* Add test with '.js' extension

* Handle case-insensitive paths

* Better tsconfig handling

* Handle properties inside compilerOptions

* Use getOptionFromName
  • Loading branch information
Andy authored Jun 1, 2018
1 parent 19ef194 commit 8dc7d5a
Show file tree
Hide file tree
Showing 29 changed files with 781 additions and 143 deletions.
3 changes: 2 additions & 1 deletion src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -929,7 +929,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.
Expand Down
37 changes: 31 additions & 6 deletions src/compiler/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2557,6 +2557,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;
Expand All @@ -2566,6 +2582,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;
}
Expand Down Expand Up @@ -2766,7 +2786,8 @@ namespace ts {
basePaths: ReadonlyArray<string>;
}

export function getFileMatcherPatterns(path: string, excludes: ReadonlyArray<string>, includes: ReadonlyArray<string>, useCaseSensitiveFileNames: boolean, currentDirectory: string): FileMatcherPatterns {
/** @param path directory of the tsconfig.json */
export function getFileMatcherPatterns(path: string, excludes: ReadonlyArray<string> | undefined, includes: ReadonlyArray<string> | undefined, useCaseSensitiveFileNames: boolean, currentDirectory: string): FileMatcherPatterns {
path = normalizePath(path);
currentDirectory = normalizePath(currentDirectory);
const absolutePath = combinePaths(currentDirectory, path);
Expand All @@ -2780,16 +2801,20 @@ namespace ts {
};
}

export function matchFiles(path: string, extensions: ReadonlyArray<string>, excludes: ReadonlyArray<string>, includes: ReadonlyArray<string>, useCaseSensitiveFileNames: boolean, currentDirectory: string, depth: number | undefined, getFileSystemEntries: (path: string) => FileSystemEntries): string[] {
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<string> | undefined, excludes: ReadonlyArray<string> | undefined, includes: ReadonlyArray<string> | 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].
Expand Down
15 changes: 9 additions & 6 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1082,7 +1082,7 @@ namespace ts {
}

export function getPropertyAssignment(objectLiteral: ObjectLiteralExpression, key: string, key2?: string): ReadonlyArray<PropertyAssignment> {
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);
Expand All @@ -1098,12 +1098,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<PropertyAssignment> {
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 {
Expand Down
20 changes: 10 additions & 10 deletions src/harness/fourslash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3149,8 +3149,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<ts.FileTextChanges>, 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.`);
}
Expand All @@ -3167,8 +3171,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));
}
}

Expand Down Expand Up @@ -3378,12 +3382,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<ts.ApplicableRefactorInfo> {
Expand Down
14 changes: 11 additions & 3 deletions src/harness/unittests/tsserverProjectSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8407,15 +8407,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<ReadonlyArray<FileTextChanges>>(edits, [{
fileName: "/user.ts",
textChanges: [{
Expand Down
2 changes: 1 addition & 1 deletion src/server/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1695,7 +1695,7 @@ namespace ts.server {

private getEditsForFileRename(args: protocol.GetEditsForFileRenameRequestArgs, simplifiedResult: boolean): ReadonlyArray<protocol.FileCodeEdits> | ReadonlyArray<FileTextChanges> {
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;
}

Expand Down
Loading

0 comments on commit 8dc7d5a

Please sign in to comment.