getEditsForFileRename: Handle all projects and source-mapped files (#25522)

* getEditsForFileRename: Handle all projects and source-mapped files

* Update API (#24966)

* Use areEqual
This commit is contained in:
Andy
2018-07-09 17:58:02 -07:00
committed by GitHub
parent e532f53189
commit e9e5ebee73
7 changed files with 87 additions and 27 deletions
+1 -1
View File
@@ -3369,7 +3369,7 @@ Actual: ${stringify(fullActual)}`);
this.languageServiceAdapterHost.renameFileOrDirectory(oldPath, newPath);
this.languageService.cleanupSemanticCache();
const pathUpdater = ts.getPathUpdater(oldPath, newPath, ts.createGetCanonicalFileName(/*useCaseSensitiveFileNames*/ false));
const pathUpdater = ts.getPathUpdater(oldPath, newPath, ts.createGetCanonicalFileName(/*useCaseSensitiveFileNames*/ false), /*sourceMapper*/ undefined);
test(renameKeys(newFileContents, key => pathUpdater(key) || key), "with file moved");
}
+1 -1
View File
@@ -155,7 +155,7 @@ namespace Harness.LanguageService {
this.vfs.mkdirpSync(ts.getDirectoryPath(newPath));
this.vfs.renameSync(oldPath, newPath);
const updater = ts.getPathUpdater(oldPath, newPath, ts.createGetCanonicalFileName(this.useCaseSensitiveFileNames()));
const updater = ts.getPathUpdater(oldPath, newPath, ts.createGetCanonicalFileName(this.useCaseSensitiveFileNames()), /*sourceMapper*/ undefined);
this.scriptInfos.forEach((scriptInfo, key) => {
const newFileName = updater(key);
if (newFileName !== undefined) {
+35 -15
View File
@@ -286,6 +286,16 @@ namespace ts.server {
: deduplicate(outputs, areEqual);
}
function combineProjectOutputFromEveryProject<T>(projectService: ProjectService, action: (project: Project) => ReadonlyArray<T>, areEqual: (a: T, b: T) => boolean) {
const outputs: T[] = [];
projectService.forEachProject(project => {
if (project.isOrphan() || !project.languageServiceEnabled) return;
const theseOutputs = action(project);
outputs.push(...theseOutputs.filter(output => !outputs.some(o => areEqual(o, output))));
});
return outputs;
}
function combineProjectOutputWhileOpeningReferencedProjects<T>(
projects: Projects,
projectService: ProjectService,
@@ -1749,19 +1759,11 @@ namespace ts.server {
const newPath = toNormalizedPath(args.newFilePath);
const formatOptions = this.getHostFormatOptions();
const preferences = this.getHostPreferences();
const changes: (protocol.FileCodeEdits | FileTextChanges)[] = [];
this.projectService.forEachProject(project => {
if (project.isOrphan() || !project.languageServiceEnabled) return;
for (const fileTextChanges of project.getLanguageService().getEditsForFileRename(oldPath, newPath, formatOptions, preferences)) {
// Subsequent projects may make conflicting edits to the same file -- just go with the first.
if (!changes.some(f => f.fileName === fileTextChanges.fileName)) {
changes.push(simplifiedResult ? this.mapTextChangeToCodeEdit(project, fileTextChanges) : fileTextChanges);
}
}
});
return changes as ReadonlyArray<protocol.FileCodeEdits> | ReadonlyArray<FileTextChanges>;
const changes = combineProjectOutputFromEveryProject(
this.projectService,
project => project.getLanguageService().getEditsForFileRename(oldPath, newPath, formatOptions, preferences),
(a, b) => a.fileName === b.fileName);
return simplifiedResult ? changes.map(c => this.mapTextChangeToCodeEditUsingScriptInfo(c)) : changes;
}
private getCodeFixes(args: protocol.CodeFixRequestArgs, simplifiedResult: boolean): ReadonlyArray<protocol.CodeFixAction> | ReadonlyArray<CodeFixAction> | undefined {
@@ -1835,8 +1837,15 @@ namespace ts.server {
}
private mapTextChangeToCodeEdit(project: Project, change: FileTextChanges): protocol.FileCodeEdits {
const path = normalizedPathToPath(toNormalizedPath(change.fileName), this.host.getCurrentDirectory(), fileName => this.getCanonicalFileName(fileName));
return mapTextChangesToCodeEdits(change, project.getSourceFileOrConfigFile(path));
return mapTextChangesToCodeEdits(change, project.getSourceFileOrConfigFile(this.normalizePath(change.fileName)));
}
private mapTextChangeToCodeEditUsingScriptInfo(change: FileTextChanges): protocol.FileCodeEdits {
return mapTextChangesToCodeEditsUsingScriptInfo(change, this.projectService.getScriptInfo(this.normalizePath(change.fileName)));
}
private normalizePath(fileName: string) {
return normalizedPathToPath(toNormalizedPath(fileName), this.host.getCurrentDirectory(), fileName => this.getCanonicalFileName(fileName));
}
private convertTextChangeToCodeEdit(change: TextChange, scriptInfo: ScriptInfo): protocol.CodeEdit {
@@ -2361,6 +2370,13 @@ namespace ts.server {
}
}
function mapTextChangesToCodeEditsUsingScriptInfo(textChanges: FileTextChanges, scriptInfo: ScriptInfo | undefined): protocol.FileCodeEdits {
Debug.assert(!!textChanges.isNewFile === !scriptInfo);
return scriptInfo
? { fileName: textChanges.fileName, textChanges: textChanges.textChanges.map(textChange => convertTextChangeToCodeEditUsingScriptInfo(textChange, scriptInfo)) }
: convertNewFileTextChangeToCodeEdit(textChanges);
}
function convertTextChangeToCodeEdit(change: TextChange, sourceFile: SourceFile): protocol.CodeEdit {
return {
start: convertToLocation(sourceFile.getLineAndCharacterOfPosition(change.span.start)),
@@ -2369,6 +2385,10 @@ namespace ts.server {
};
}
function convertTextChangeToCodeEditUsingScriptInfo(change: TextChange, scriptInfo: ScriptInfo) {
return { start: scriptInfo.positionToLineOffset(change.span.start), end: scriptInfo.positionToLineOffset(textSpanEnd(change.span)), newText: change.newText };
}
function convertNewFileTextChangeToCodeEdit(textChanges: FileTextChanges): protocol.FileCodeEdits {
Debug.assert(textChanges.textChanges.length === 1);
const change = first(textChanges.textChanges);
+29 -7
View File
@@ -1,10 +1,18 @@
/* @internal */
namespace ts {
export function getEditsForFileRename(program: Program, oldFileOrDirPath: string, newFileOrDirPath: string, host: LanguageServiceHost, formatContext: formatting.FormatContext, preferences: UserPreferences): ReadonlyArray<FileTextChanges> {
export function getEditsForFileRename(
program: Program,
oldFileOrDirPath: string,
newFileOrDirPath: string,
host: LanguageServiceHost,
formatContext: formatting.FormatContext,
preferences: UserPreferences,
sourceMapper: SourceMapper,
): ReadonlyArray<FileTextChanges> {
const useCaseSensitiveFileNames = hostUsesCaseSensitiveFileNames(host);
const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);
const oldToNew = getPathUpdater(oldFileOrDirPath, newFileOrDirPath, getCanonicalFileName);
const newToOld = getPathUpdater(newFileOrDirPath, oldFileOrDirPath, getCanonicalFileName);
const oldToNew = getPathUpdater(oldFileOrDirPath, newFileOrDirPath, getCanonicalFileName, sourceMapper);
const newToOld = getPathUpdater(newFileOrDirPath, oldFileOrDirPath, getCanonicalFileName, sourceMapper);
return textChanges.ChangeTracker.with({ host, formatContext }, changeTracker => {
updateTsconfigFiles(program, changeTracker, oldToNew, newFileOrDirPath, host.getCurrentDirectory(), useCaseSensitiveFileNames);
updateImports(program, changeTracker, oldToNew, newToOld, host, getCanonicalFileName, preferences);
@@ -14,13 +22,27 @@ namespace ts {
/** If 'path' refers to an old directory, returns path in the new directory. */
type PathUpdater = (path: string) => string | undefined;
// exported for tests
export function getPathUpdater(oldFileOrDirPath: string, newFileOrDirPath: string, getCanonicalFileName: GetCanonicalFileName): PathUpdater {
export function getPathUpdater(oldFileOrDirPath: string, newFileOrDirPath: string, getCanonicalFileName: GetCanonicalFileName, sourceMapper: SourceMapper | undefined): PathUpdater {
const canonicalOldPath = getCanonicalFileName(oldFileOrDirPath);
return path => {
if (getCanonicalFileName(path) === canonicalOldPath) return newFileOrDirPath;
const suffix = tryRemoveDirectoryPrefix(path, canonicalOldPath, getCanonicalFileName);
return suffix === undefined ? undefined : newFileOrDirPath + "/" + suffix;
const originalPath = sourceMapper && sourceMapper.tryGetOriginalLocation({ fileName: path, position: 0 });
const updatedPath = getUpdatedPath(originalPath ? originalPath.fileName : path);
return originalPath
? updatedPath === undefined ? undefined : makeCorrespondingRelativeChange(originalPath.fileName, updatedPath, path, getCanonicalFileName)
: updatedPath;
};
function getUpdatedPath(pathToUpdate: string): string | undefined {
if (getCanonicalFileName(pathToUpdate) === canonicalOldPath) return newFileOrDirPath;
const suffix = tryRemoveDirectoryPrefix(pathToUpdate, canonicalOldPath, getCanonicalFileName);
return suffix === undefined ? undefined : newFileOrDirPath + "/" + suffix;
}
}
// Relative path from a0 to b0 should be same as relative path from a1 to b1. Returns b1.
function makeCorrespondingRelativeChange(a0: string, b0: string, a1: string, getCanonicalFileName: GetCanonicalFileName): string {
const rel = getRelativePathFromFile(a0, b0, getCanonicalFileName);
return combinePathsSafe(getDirectoryPath(a1), rel);
}
function updateTsconfigFiles(program: Program, changeTracker: textChanges.ChangeTracker, oldToNew: PathUpdater, newFileOrDirPath: string, currentDirectory: string, useCaseSensitiveFileNames: boolean): void {
+1 -1
View File
@@ -1812,7 +1812,7 @@ namespace ts {
}
function getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences = emptyOptions): ReadonlyArray<FileTextChanges> {
return ts.getEditsForFileRename(getProgram()!, oldFilePath, newFilePath, host, formatting.getFormatContext(formatOptions), preferences);
return ts.getEditsForFileRename(getProgram()!, oldFilePath, newFilePath, host, formatting.getFormatContext(formatOptions), preferences, sourceMapper);
}
function applyCodeActionCommand(action: CodeActionCommand): Promise<ApplyCodeActionCommandResult>;
@@ -9210,6 +9210,22 @@ export function Test2() {
});
});
it("getEditsForFileRename", () => {
const { session, aTs, userTs } = makeSampleProjects();
const response = executeSessionRequest<protocol.GetEditsForFileRenameRequest, protocol.GetEditsForFileRenameResponse>(session, protocol.CommandTypes.GetEditsForFileRename, {
oldFilePath: aTs.path,
newFilePath: "/a/aNew.ts",
});
assert.deepEqual<ReadonlyArray<protocol.FileCodeEdits>>(response, [
{
fileName: userTs.path,
textChanges: [
{ ...protocolTextSpanFromSubstring(userTs.content, "../a/bin/a"), newText: "../a/bin/aNew" },
],
},
]);
});
});
function makeReferenceItem(file: File, isDefinition: boolean, text: string, lineText: string, options?: SpanFromSubstringOptions): protocol.ReferencesResponseItem {
+4 -2
View File
@@ -11215,10 +11215,10 @@ declare namespace ts.FindAllReferences.Core {
function getReferenceEntriesForShorthandPropertyAssignment(node: Node, checker: TypeChecker, addReference: (node: Node) => void): void;
}
declare namespace ts {
function getEditsForFileRename(program: Program, oldFileOrDirPath: string, newFileOrDirPath: string, host: LanguageServiceHost, formatContext: formatting.FormatContext, preferences: UserPreferences): ReadonlyArray<FileTextChanges>;
function getEditsForFileRename(program: Program, oldFileOrDirPath: string, newFileOrDirPath: string, host: LanguageServiceHost, formatContext: formatting.FormatContext, preferences: UserPreferences, sourceMapper: SourceMapper): ReadonlyArray<FileTextChanges>;
/** 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;
function getPathUpdater(oldFileOrDirPath: string, newFileOrDirPath: string, getCanonicalFileName: GetCanonicalFileName, sourceMapper: SourceMapper | undefined): PathUpdater;
}
declare namespace ts.GoToDefinition {
function getDefinitionAtPosition(program: Program, sourceFile: SourceFile, position: number): DefinitionInfo[] | undefined;
@@ -14253,6 +14253,8 @@ declare namespace ts.server {
private mapCodeFixAction;
private mapTextChangesToCodeEdits;
private mapTextChangeToCodeEdit;
private mapTextChangeToCodeEditUsingScriptInfo;
private normalizePath;
private convertTextChangeToCodeEdit;
private getBraceMatching;
private getDiagnosticsForProject;