mirror of
https://github.com/microsoft/TypeScript.git
synced 2025-11-18 17:21:48 +00:00
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:
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user