Skip to content

Commit e9e5ebe

Browse files
author
Andy
authored
getEditsForFileRename: Handle all projects and source-mapped files (#25522)
* getEditsForFileRename: Handle all projects and source-mapped files * Update API (#24966) * Use areEqual
1 parent e532f53 commit e9e5ebe

File tree

7 files changed

+87
-27
lines changed

7 files changed

+87
-27
lines changed

src/harness/fourslash.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3369,7 +3369,7 @@ Actual: ${stringify(fullActual)}`);
33693369

33703370
this.languageServiceAdapterHost.renameFileOrDirectory(oldPath, newPath);
33713371
this.languageService.cleanupSemanticCache();
3372-
const pathUpdater = ts.getPathUpdater(oldPath, newPath, ts.createGetCanonicalFileName(/*useCaseSensitiveFileNames*/ false));
3372+
const pathUpdater = ts.getPathUpdater(oldPath, newPath, ts.createGetCanonicalFileName(/*useCaseSensitiveFileNames*/ false), /*sourceMapper*/ undefined);
33733373
test(renameKeys(newFileContents, key => pathUpdater(key) || key), "with file moved");
33743374
}
33753375

src/harness/harnessLanguageService.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ namespace Harness.LanguageService {
155155
this.vfs.mkdirpSync(ts.getDirectoryPath(newPath));
156156
this.vfs.renameSync(oldPath, newPath);
157157

158-
const updater = ts.getPathUpdater(oldPath, newPath, ts.createGetCanonicalFileName(this.useCaseSensitiveFileNames()));
158+
const updater = ts.getPathUpdater(oldPath, newPath, ts.createGetCanonicalFileName(this.useCaseSensitiveFileNames()), /*sourceMapper*/ undefined);
159159
this.scriptInfos.forEach((scriptInfo, key) => {
160160
const newFileName = updater(key);
161161
if (newFileName !== undefined) {

src/server/session.ts

+35-15
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,16 @@ namespace ts.server {
286286
: deduplicate(outputs, areEqual);
287287
}
288288

289+
function combineProjectOutputFromEveryProject<T>(projectService: ProjectService, action: (project: Project) => ReadonlyArray<T>, areEqual: (a: T, b: T) => boolean) {
290+
const outputs: T[] = [];
291+
projectService.forEachProject(project => {
292+
if (project.isOrphan() || !project.languageServiceEnabled) return;
293+
const theseOutputs = action(project);
294+
outputs.push(...theseOutputs.filter(output => !outputs.some(o => areEqual(o, output))));
295+
});
296+
return outputs;
297+
}
298+
289299
function combineProjectOutputWhileOpeningReferencedProjects<T>(
290300
projects: Projects,
291301
projectService: ProjectService,
@@ -1749,19 +1759,11 @@ namespace ts.server {
17491759
const newPath = toNormalizedPath(args.newFilePath);
17501760
const formatOptions = this.getHostFormatOptions();
17511761
const preferences = this.getHostPreferences();
1752-
1753-
const changes: (protocol.FileCodeEdits | FileTextChanges)[] = [];
1754-
this.projectService.forEachProject(project => {
1755-
if (project.isOrphan() || !project.languageServiceEnabled) return;
1756-
for (const fileTextChanges of project.getLanguageService().getEditsForFileRename(oldPath, newPath, formatOptions, preferences)) {
1757-
// Subsequent projects may make conflicting edits to the same file -- just go with the first.
1758-
if (!changes.some(f => f.fileName === fileTextChanges.fileName)) {
1759-
changes.push(simplifiedResult ? this.mapTextChangeToCodeEdit(project, fileTextChanges) : fileTextChanges);
1760-
}
1761-
}
1762-
});
1763-
1764-
return changes as ReadonlyArray<protocol.FileCodeEdits> | ReadonlyArray<FileTextChanges>;
1762+
const changes = combineProjectOutputFromEveryProject(
1763+
this.projectService,
1764+
project => project.getLanguageService().getEditsForFileRename(oldPath, newPath, formatOptions, preferences),
1765+
(a, b) => a.fileName === b.fileName);
1766+
return simplifiedResult ? changes.map(c => this.mapTextChangeToCodeEditUsingScriptInfo(c)) : changes;
17651767
}
17661768

17671769
private getCodeFixes(args: protocol.CodeFixRequestArgs, simplifiedResult: boolean): ReadonlyArray<protocol.CodeFixAction> | ReadonlyArray<CodeFixAction> | undefined {
@@ -1835,8 +1837,15 @@ namespace ts.server {
18351837
}
18361838

18371839
private mapTextChangeToCodeEdit(project: Project, change: FileTextChanges): protocol.FileCodeEdits {
1838-
const path = normalizedPathToPath(toNormalizedPath(change.fileName), this.host.getCurrentDirectory(), fileName => this.getCanonicalFileName(fileName));
1839-
return mapTextChangesToCodeEdits(change, project.getSourceFileOrConfigFile(path));
1840+
return mapTextChangesToCodeEdits(change, project.getSourceFileOrConfigFile(this.normalizePath(change.fileName)));
1841+
}
1842+
1843+
private mapTextChangeToCodeEditUsingScriptInfo(change: FileTextChanges): protocol.FileCodeEdits {
1844+
return mapTextChangesToCodeEditsUsingScriptInfo(change, this.projectService.getScriptInfo(this.normalizePath(change.fileName)));
1845+
}
1846+
1847+
private normalizePath(fileName: string) {
1848+
return normalizedPathToPath(toNormalizedPath(fileName), this.host.getCurrentDirectory(), fileName => this.getCanonicalFileName(fileName));
18401849
}
18411850

18421851
private convertTextChangeToCodeEdit(change: TextChange, scriptInfo: ScriptInfo): protocol.CodeEdit {
@@ -2361,6 +2370,13 @@ namespace ts.server {
23612370
}
23622371
}
23632372

2373+
function mapTextChangesToCodeEditsUsingScriptInfo(textChanges: FileTextChanges, scriptInfo: ScriptInfo | undefined): protocol.FileCodeEdits {
2374+
Debug.assert(!!textChanges.isNewFile === !scriptInfo);
2375+
return scriptInfo
2376+
? { fileName: textChanges.fileName, textChanges: textChanges.textChanges.map(textChange => convertTextChangeToCodeEditUsingScriptInfo(textChange, scriptInfo)) }
2377+
: convertNewFileTextChangeToCodeEdit(textChanges);
2378+
}
2379+
23642380
function convertTextChangeToCodeEdit(change: TextChange, sourceFile: SourceFile): protocol.CodeEdit {
23652381
return {
23662382
start: convertToLocation(sourceFile.getLineAndCharacterOfPosition(change.span.start)),
@@ -2369,6 +2385,10 @@ namespace ts.server {
23692385
};
23702386
}
23712387

2388+
function convertTextChangeToCodeEditUsingScriptInfo(change: TextChange, scriptInfo: ScriptInfo) {
2389+
return { start: scriptInfo.positionToLineOffset(change.span.start), end: scriptInfo.positionToLineOffset(textSpanEnd(change.span)), newText: change.newText };
2390+
}
2391+
23722392
function convertNewFileTextChangeToCodeEdit(textChanges: FileTextChanges): protocol.FileCodeEdits {
23732393
Debug.assert(textChanges.textChanges.length === 1);
23742394
const change = first(textChanges.textChanges);

src/services/getEditsForFileRename.ts

+29-7
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
/* @internal */
22
namespace ts {
3-
export function getEditsForFileRename(program: Program, oldFileOrDirPath: string, newFileOrDirPath: string, host: LanguageServiceHost, formatContext: formatting.FormatContext, preferences: UserPreferences): ReadonlyArray<FileTextChanges> {
3+
export function getEditsForFileRename(
4+
program: Program,
5+
oldFileOrDirPath: string,
6+
newFileOrDirPath: string,
7+
host: LanguageServiceHost,
8+
formatContext: formatting.FormatContext,
9+
preferences: UserPreferences,
10+
sourceMapper: SourceMapper,
11+
): ReadonlyArray<FileTextChanges> {
412
const useCaseSensitiveFileNames = hostUsesCaseSensitiveFileNames(host);
513
const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);
6-
const oldToNew = getPathUpdater(oldFileOrDirPath, newFileOrDirPath, getCanonicalFileName);
7-
const newToOld = getPathUpdater(newFileOrDirPath, oldFileOrDirPath, getCanonicalFileName);
14+
const oldToNew = getPathUpdater(oldFileOrDirPath, newFileOrDirPath, getCanonicalFileName, sourceMapper);
15+
const newToOld = getPathUpdater(newFileOrDirPath, oldFileOrDirPath, getCanonicalFileName, sourceMapper);
816
return textChanges.ChangeTracker.with({ host, formatContext }, changeTracker => {
917
updateTsconfigFiles(program, changeTracker, oldToNew, newFileOrDirPath, host.getCurrentDirectory(), useCaseSensitiveFileNames);
1018
updateImports(program, changeTracker, oldToNew, newToOld, host, getCanonicalFileName, preferences);
@@ -14,13 +22,27 @@ namespace ts {
1422
/** If 'path' refers to an old directory, returns path in the new directory. */
1523
type PathUpdater = (path: string) => string | undefined;
1624
// exported for tests
17-
export function getPathUpdater(oldFileOrDirPath: string, newFileOrDirPath: string, getCanonicalFileName: GetCanonicalFileName): PathUpdater {
25+
export function getPathUpdater(oldFileOrDirPath: string, newFileOrDirPath: string, getCanonicalFileName: GetCanonicalFileName, sourceMapper: SourceMapper | undefined): PathUpdater {
1826
const canonicalOldPath = getCanonicalFileName(oldFileOrDirPath);
1927
return path => {
20-
if (getCanonicalFileName(path) === canonicalOldPath) return newFileOrDirPath;
21-
const suffix = tryRemoveDirectoryPrefix(path, canonicalOldPath, getCanonicalFileName);
22-
return suffix === undefined ? undefined : newFileOrDirPath + "/" + suffix;
28+
const originalPath = sourceMapper && sourceMapper.tryGetOriginalLocation({ fileName: path, position: 0 });
29+
const updatedPath = getUpdatedPath(originalPath ? originalPath.fileName : path);
30+
return originalPath
31+
? updatedPath === undefined ? undefined : makeCorrespondingRelativeChange(originalPath.fileName, updatedPath, path, getCanonicalFileName)
32+
: updatedPath;
2333
};
34+
35+
function getUpdatedPath(pathToUpdate: string): string | undefined {
36+
if (getCanonicalFileName(pathToUpdate) === canonicalOldPath) return newFileOrDirPath;
37+
const suffix = tryRemoveDirectoryPrefix(pathToUpdate, canonicalOldPath, getCanonicalFileName);
38+
return suffix === undefined ? undefined : newFileOrDirPath + "/" + suffix;
39+
}
40+
}
41+
42+
// Relative path from a0 to b0 should be same as relative path from a1 to b1. Returns b1.
43+
function makeCorrespondingRelativeChange(a0: string, b0: string, a1: string, getCanonicalFileName: GetCanonicalFileName): string {
44+
const rel = getRelativePathFromFile(a0, b0, getCanonicalFileName);
45+
return combinePathsSafe(getDirectoryPath(a1), rel);
2446
}
2547

2648
function updateTsconfigFiles(program: Program, changeTracker: textChanges.ChangeTracker, oldToNew: PathUpdater, newFileOrDirPath: string, currentDirectory: string, useCaseSensitiveFileNames: boolean): void {

src/services/services.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1812,7 +1812,7 @@ namespace ts {
18121812
}
18131813

18141814
function getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences = emptyOptions): ReadonlyArray<FileTextChanges> {
1815-
return ts.getEditsForFileRename(getProgram()!, oldFilePath, newFilePath, host, formatting.getFormatContext(formatOptions), preferences);
1815+
return ts.getEditsForFileRename(getProgram()!, oldFilePath, newFilePath, host, formatting.getFormatContext(formatOptions), preferences, sourceMapper);
18161816
}
18171817

18181818
function applyCodeActionCommand(action: CodeActionCommand): Promise<ApplyCodeActionCommandResult>;

src/testRunner/unittests/tsserverProjectSystem.ts

+16
Original file line numberDiff line numberDiff line change
@@ -9210,6 +9210,22 @@ export function Test2() {
92109210
});
92119211

92129212
});
9213+
9214+
it("getEditsForFileRename", () => {
9215+
const { session, aTs, userTs } = makeSampleProjects();
9216+
const response = executeSessionRequest<protocol.GetEditsForFileRenameRequest, protocol.GetEditsForFileRenameResponse>(session, protocol.CommandTypes.GetEditsForFileRename, {
9217+
oldFilePath: aTs.path,
9218+
newFilePath: "/a/aNew.ts",
9219+
});
9220+
assert.deepEqual<ReadonlyArray<protocol.FileCodeEdits>>(response, [
9221+
{
9222+
fileName: userTs.path,
9223+
textChanges: [
9224+
{ ...protocolTextSpanFromSubstring(userTs.content, "../a/bin/a"), newText: "../a/bin/aNew" },
9225+
],
9226+
},
9227+
]);
9228+
});
92139229
});
92149230

92159231
function makeReferenceItem(file: File, isDefinition: boolean, text: string, lineText: string, options?: SpanFromSubstringOptions): protocol.ReferencesResponseItem {

tests/baselines/reference/api/tsserverlibrary.d.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -11215,10 +11215,10 @@ declare namespace ts.FindAllReferences.Core {
1121511215
function getReferenceEntriesForShorthandPropertyAssignment(node: Node, checker: TypeChecker, addReference: (node: Node) => void): void;
1121611216
}
1121711217
declare namespace ts {
11218-
function getEditsForFileRename(program: Program, oldFileOrDirPath: string, newFileOrDirPath: string, host: LanguageServiceHost, formatContext: formatting.FormatContext, preferences: UserPreferences): ReadonlyArray<FileTextChanges>;
11218+
function getEditsForFileRename(program: Program, oldFileOrDirPath: string, newFileOrDirPath: string, host: LanguageServiceHost, formatContext: formatting.FormatContext, preferences: UserPreferences, sourceMapper: SourceMapper): ReadonlyArray<FileTextChanges>;
1121911219
/** If 'path' refers to an old directory, returns path in the new directory. */
1122011220
type PathUpdater = (path: string) => string | undefined;
11221-
function getPathUpdater(oldFileOrDirPath: string, newFileOrDirPath: string, getCanonicalFileName: GetCanonicalFileName): PathUpdater;
11221+
function getPathUpdater(oldFileOrDirPath: string, newFileOrDirPath: string, getCanonicalFileName: GetCanonicalFileName, sourceMapper: SourceMapper | undefined): PathUpdater;
1122211222
}
1122311223
declare namespace ts.GoToDefinition {
1122411224
function getDefinitionAtPosition(program: Program, sourceFile: SourceFile, position: number): DefinitionInfo[] | undefined;
@@ -14253,6 +14253,8 @@ declare namespace ts.server {
1425314253
private mapCodeFixAction;
1425414254
private mapTextChangesToCodeEdits;
1425514255
private mapTextChangeToCodeEdit;
14256+
private mapTextChangeToCodeEditUsingScriptInfo;
14257+
private normalizePath;
1425614258
private convertTextChangeToCodeEdit;
1425714259
private getBraceMatching;
1425814260
private getDiagnosticsForProject;

0 commit comments

Comments
 (0)