Skip to content

Commit

Permalink
Add 'renameFile' command to services
Browse files Browse the repository at this point in the history
  • Loading branch information
Andy Hanson committed Apr 20, 2018
1 parent 18c3f5f commit 95525be
Show file tree
Hide file tree
Showing 21 changed files with 175 additions and 15 deletions.
9 changes: 9 additions & 0 deletions src/compiler/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2212,6 +2212,15 @@ namespace ts {
return absolutePath;
}

export function getRelativePath(path: string, directoryPath: string, getCanonicalFileName: GetCanonicalFileName) {
const relativePath = getRelativePathToDirectoryOrUrl(directoryPath, path, directoryPath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false);
return ensurePathIsRelative(relativePath);
}

export function ensurePathIsRelative(path: string): string {
return !pathIsRelative(path) ? "./" + path : path;
}

export function getBaseFileName(path: string) {
if (path === undefined) {
return undefined;
Expand Down
6 changes: 6 additions & 0 deletions src/compiler/moduleNameResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,12 @@ namespace ts {
}
}

export function resolveModuleNameFromCache(moduleName: string, containingFile: string, cache: ModuleResolutionCache): ResolvedModuleWithFailedLookupLocations | undefined {
const containingDirectory = getDirectoryPath(containingFile);
const perFolderCache = cache && cache.getOrCreateCacheForDirectory(containingDirectory);
return perFolderCache && perFolderCache.get(moduleName);
}

export function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache): ResolvedModuleWithFailedLookupLocations {
const traceEnabled = isTraceEnabled(compilerOptions, host);
if (traceEnabled) {
Expand Down
10 changes: 6 additions & 4 deletions src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -622,9 +622,6 @@ namespace ts {

Debug.assert(!!missingFilePaths);

// unconditionally set moduleResolutionCache to undefined to avoid unnecessary leaks
moduleResolutionCache = undefined;

// Release any files we have acquired in the old program but are
// not part of the new program.
if (oldProgram && host.onReleaseOldSourceFile) {
Expand Down Expand Up @@ -670,7 +667,8 @@ namespace ts {
sourceFileToPackageName,
redirectTargetsSet,
isEmittedFile,
getConfigFileParsingDiagnostics
getConfigFileParsingDiagnostics,
getResolvedModuleWithFailedLookupLocationsFromCache,
};

verifyCompilerOptions();
Expand All @@ -679,6 +677,10 @@ namespace ts {

return program;

function getResolvedModuleWithFailedLookupLocationsFromCache(moduleName: string, containingFile: string): ResolvedModuleWithFailedLookupLocations {
return moduleResolutionCache && resolveModuleNameFromCache(moduleName, containingFile, moduleResolutionCache);
}

function toPath(fileName: string): Path {
return ts.toPath(fileName, currentDirectory, getCanonicalFileName);
}
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2725,6 +2725,8 @@ namespace ts {
/* @internal */ redirectTargetsSet: Map<true>;
/** Is the file emitted file */
/* @internal */ isEmittedFile(file: string): boolean;

/* @internal */ getResolvedModuleWithFailedLookupLocationsFromCache(moduleName: string, containingFile: string): ResolvedModuleWithFailedLookupLocations | undefined;
}

/* @internal */
Expand Down
19 changes: 19 additions & 0 deletions src/harness/fourslash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3279,6 +3279,15 @@ Actual: ${stringify(fullActual)}`);
private static textSpansEqual(a: ts.TextSpan, b: ts.TextSpan) {
return a && b && a.start === b.start && a.length === b.length;
}

public renameFile(options: FourSlashInterface.RenameFileOptions): void {
const changes = this.languageService.renameFile(options.oldPath, options.newPath, this.formatCodeSettings);
this.applyChanges(changes);
for (const fileName in options.newFileContents) {
this.openFile(fileName);
this.verifyCurrentFileContent(options.newFileContents[fileName]);
}
}
}

export function runFourSlashTest(basePath: string, testType: FourSlashTestType, fileName: string) {
Expand Down Expand Up @@ -4370,6 +4379,10 @@ namespace FourSlashInterface {
public allRangesAppearInImplementationList(markerName: string) {
this.state.verifyRangesInImplementationList(markerName);
}

public renameFile(options: RenameFileOptions) {
this.state.renameFile(options);
}
}

export class Edit {
Expand Down Expand Up @@ -4710,4 +4723,10 @@ namespace FourSlashInterface {
range?: FourSlash.Range;
code: number;
}

export interface RenameFileOptions {
readonly oldPath: string;
readonly newPath: string;
readonly newFileContents: { readonly [fileName: string]: string };
}
}
3 changes: 3 additions & 0 deletions src/harness/harnessLanguageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,9 @@ namespace Harness.LanguageService {
organizeImports(_scope: ts.OrganizeImportsScope, _formatOptions: ts.FormatCodeSettings): ReadonlyArray<ts.FileTextChanges> {
throw new Error("Not supported on the shim.");
}
renameFile(): ReadonlyArray<ts.FileTextChanges> {
throw new Error("Not supported on the shim.");
}
getEmitOutput(fileName: string): ts.EmitOutput {
return unwrapJSONCallResult(this.shim.getEmitOutput(fileName));
}
Expand Down
1 change: 1 addition & 0 deletions src/harness/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"../services/navigateTo.ts",
"../services/navigationBar.ts",
"../services/organizeImports.ts",
"../services/renameFile.ts",
"../services/outliningElementsCollector.ts",
"../services/patternMatcher.ts",
"../services/preProcess.ts",
Expand Down
2 changes: 2 additions & 0 deletions src/harness/unittests/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,8 @@ namespace ts.server {
CommandNames.GetEditsForRefactorFull,
CommandNames.OrganizeImports,
CommandNames.OrganizeImportsFull,
CommandNames.RenameFile,
CommandNames.RenameFileFull,
];

it("should not throw when commands are executed with invalid arguments", () => {
Expand Down
4 changes: 4 additions & 0 deletions src/server/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,10 @@ namespace ts.server {
return notImplemented();
}

renameFile() {
return notImplemented();
}

private convertCodeEditsToTextChanges(edits: protocol.FileCodeEdits[]): FileTextChanges[] {
return edits.map(edit => {
const fileName = edit.fileName;
Expand Down
19 changes: 19 additions & 0 deletions src/server/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ namespace ts.server.protocol {
OrganizeImports = "organizeImports",
/* @internal */
OrganizeImportsFull = "organizeImports-full",
RenameFile = "renameFile",
/* @internal */
RenameFileFull = "renameFile-full",

// NOTE: If updating this, be sure to also update `allCommandNames` in `harness/unittests/session.ts`.
}
Expand Down Expand Up @@ -610,6 +613,22 @@ namespace ts.server.protocol {
edits: ReadonlyArray<FileCodeEdits>;
}

export interface RenameFileRequest extends Request {
command: CommandTypes.RenameFile;
arguments: RenameFileRequestArgs;
}

// Note: The file from FileRequestArgs is just any file in the project.
// We will generate code changes for every file in that project, so the choice is arbitrary.
export interface RenameFileRequestArgs extends FileRequestArgs {
readonly oldFilePath: string;
readonly newFilePath: string;
}

export interface RenameFileResponse extends Response {
edits: ReadonlyArray<FileCodeEdits>;
}

/**
* Request for the available codefixes at a specific position.
*/
Expand Down
14 changes: 13 additions & 1 deletion src/server/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1664,6 +1664,12 @@ namespace ts.server {
}
}

private renameFile(args: protocol.RenameFileRequestArgs, simplifiedResult: boolean): ReadonlyArray<protocol.FileCodeEdits> | ReadonlyArray<FileTextChanges> {
const { file, project } = this.getFileAndProject(args);
const changes = project.getLanguageService().renameFile(args.oldFilePath, args.newFilePath, this.getFormatOptions(file));
return simplifiedResult ? this.mapTextChangesToCodeEdits(project, changes) : changes;
}

private getCodeFixes(args: protocol.CodeFixRequestArgs, simplifiedResult: boolean): ReadonlyArray<protocol.CodeFixAction> | ReadonlyArray<CodeFixAction> {
if (args.errorCodes.length === 0) {
return undefined;
Expand Down Expand Up @@ -2117,7 +2123,13 @@ namespace ts.server {
},
[CommandNames.OrganizeImportsFull]: (request: protocol.OrganizeImportsRequest) => {
return this.requiredResponse(this.organizeImports(request.arguments, /*simplifiedResult*/ false));
}
},
[CommandNames.RenameFile]: (request: protocol.RenameFileRequest) => {
return this.requiredResponse(this.renameFile(request.arguments, /*simplifiedResult*/ true));
},
[CommandNames.RenameFileFull]: (request: protocol.RenameFileRequest) => {
return this.requiredResponse(this.renameFile(request.arguments, /*simplifiedResult*/ false));
},
});

public addProtocolHandler(command: string, handler: (request: protocol.Request) => HandlerResponse) {
Expand Down
1 change: 1 addition & 0 deletions src/server/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"../services/navigateTo.ts",
"../services/navigationBar.ts",
"../services/organizeImports.ts",
"../services/renameFile.ts",
"../services/outliningElementsCollector.ts",
"../services/patternMatcher.ts",
"../services/preProcess.ts",
Expand Down
1 change: 1 addition & 0 deletions src/server/tsconfig.library.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"../services/navigateTo.ts",
"../services/navigationBar.ts",
"../services/organizeImports.ts",
"../services/renameFile.ts",
"../services/outliningElementsCollector.ts",
"../services/patternMatcher.ts",
"../services/preProcess.ts",
Expand Down
8 changes: 1 addition & 7 deletions src/services/codefixes/importFixes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ namespace ts.codefix {
}

function convertToImportCodeFixContext(context: CodeFixContext, symbolToken: Node, symbolName: string): ImportCodeFixContext {
const useCaseSensitiveFileNames = context.host.useCaseSensitiveFileNames ? context.host.useCaseSensitiveFileNames() : false;
const { program } = context;
const checker = program.getTypeChecker();

Expand All @@ -51,7 +50,7 @@ namespace ts.codefix {
checker,
compilerOptions: program.getCompilerOptions(),
cachedImportDeclarations: [],
getCanonicalFileName: createGetCanonicalFileName(useCaseSensitiveFileNames),
getCanonicalFileName: createGetCanonicalFileName(hostUsesCaseSensitiveFileNames(context.host)),
symbolName,
symbolToken,
preferences: context.preferences,
Expand Down Expand Up @@ -547,11 +546,6 @@ namespace ts.codefix {
return startsWith(path, "..");
}

function getRelativePath(path: string, directoryPath: string, getCanonicalFileName: GetCanonicalFileName) {
const relativePath = getRelativePathToDirectoryOrUrl(directoryPath, path, directoryPath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false);
return !pathIsRelative(relativePath) ? "./" + relativePath : relativePath;
}

function getCodeActionsForAddImport(
exportInfos: ReadonlyArray<SymbolExportInfo>,
ctx: ImportCodeFixContext,
Expand Down
45 changes: 45 additions & 0 deletions src/services/renameFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/* @internal */
namespace ts {
export function renameFile(program: Program, oldFilePath: string, newFilePath: string, host: LanguageServiceHost, formatContext: formatting.FormatContext): ReadonlyArray<FileTextChanges> {
const pathUpdater = getPathUpdater(oldFilePath, newFilePath, host);
return textChanges.ChangeTracker.with({ host, formatContext }, changeTracker => {
const importsToUpdate = getImportsToUpdate(program, oldFilePath);
for (const importToUpdate of importsToUpdate) {
const newPath = pathUpdater(importToUpdate.text);
if (newPath !== undefined) {
changeTracker.replaceNode(importToUpdate.getSourceFile(), importToUpdate, updateStringLiteralLike(importToUpdate, newPath));
}
}
});
}

function getImportsToUpdate(program: Program, oldFilePath: string): ReadonlyArray<StringLiteralLike> {
const checker = program.getTypeChecker();
const result: StringLiteralLike[] = [];
for (const file of program.getSourceFiles()) {
for (const importStringLiteral of file.imports) {
// If it resolved to something already, ignore.
if (checker.getSymbolAtLocation(importStringLiteral)) continue;

const resolved = program.getResolvedModuleWithFailedLookupLocationsFromCache(importStringLiteral.text, file.fileName);
if (contains(resolved.failedLookupLocations, oldFilePath)) {
result.push(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 = removeFileExtension(getRelativePath(newFilePath, getDirectoryPath(oldFilePath), createGetCanonicalFileName(hostUsesCaseSensitiveFileNames(host))));
return oldPath => {
if (!pathIsRelative(oldPath)) return;
return ensurePathIsRelative(normalizePath(combinePaths(getDirectoryPath(oldPath), rel)));
};
}

function updateStringLiteralLike(old: StringLiteralLike, newText: string): StringLiteralLike {
return old.kind === SyntaxKind.StringLiteral ? createLiteral(newText, /*isSingleQuote*/ old.singleQuote) : createNoSubstitutionTemplateLiteral(newText);
}
}
11 changes: 8 additions & 3 deletions src/services/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1128,7 +1128,6 @@ namespace ts {
let lastProjectVersion: string;
let lastTypesRootVersion = 0;

const useCaseSensitivefileNames = host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames();
const cancellationToken = new CancellationTokenObject(host.getCancellationToken && host.getCancellationToken());

const currentDirectory = host.getCurrentDirectory();
Expand All @@ -1145,7 +1144,8 @@ namespace ts {
}
}

const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitivefileNames);
const useCaseSensitiveFileNames = hostUsesCaseSensitiveFileNames(host);
const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);

function getValidSourceFile(fileName: string): SourceFile {
const sourceFile = program.getSourceFile(fileName);
Expand Down Expand Up @@ -1202,7 +1202,7 @@ namespace ts {
getSourceFileByPath: getOrCreateSourceFileByPath,
getCancellationToken: () => cancellationToken,
getCanonicalFileName,
useCaseSensitiveFileNames: () => useCaseSensitivefileNames,
useCaseSensitiveFileNames: () => useCaseSensitiveFileNames,
getNewLine: () => getNewLineCharacter(newSettings, () => getNewLineOrDefaultFromHost(host)),
getDefaultLibFileName: (options) => host.getDefaultLibFileName(options),
writeFile: noop,
Expand Down Expand Up @@ -1950,6 +1950,10 @@ namespace ts {
return OrganizeImports.organizeImports(sourceFile, formatContext, host, program, preferences);
}

function renameFile(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings): ReadonlyArray<FileTextChanges> {
return ts.renameFile(getProgram(), oldFilePath, newFilePath, host, formatting.getFormatContext(formatOptions));
}

function applyCodeActionCommand(action: CodeActionCommand): Promise<ApplyCodeActionCommandResult>;
function applyCodeActionCommand(action: CodeActionCommand[]): Promise<ApplyCodeActionCommandResult[]>;
function applyCodeActionCommand(action: CodeActionCommand | CodeActionCommand[]): Promise<ApplyCodeActionCommandResult | ApplyCodeActionCommandResult[]>;
Expand Down Expand Up @@ -2250,6 +2254,7 @@ namespace ts {
getCombinedCodeFix,
applyCodeActionCommand,
organizeImports,
renameFile,
getEmitOutput,
getNonBoundSourceFile,
getSourceFile,
Expand Down
1 change: 1 addition & 0 deletions src/services/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"navigateTo.ts",
"navigationBar.ts",
"organizeImports.ts",
"../services/renameFile.ts",
"outliningElementsCollector.ts",
"patternMatcher.ts",
"preProcess.ts",
Expand Down
1 change: 1 addition & 0 deletions src/services/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ namespace ts {
getApplicableRefactors(fileName: string, positionOrRaneg: 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<FileTextChanges>;
renameFile(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings): ReadonlyArray<FileTextChanges>;

getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput;

Expand Down
8 changes: 8 additions & 0 deletions src/services/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1213,6 +1213,14 @@ namespace ts {
? isStringOrNumericLiteral(name.expression) ? name.expression.text : undefined
: getTextOfIdentifierOrLiteral(name);
}

export function hostUsesCaseSensitiveFileNames(host: LanguageServiceHost): boolean {
return host.useCaseSensitiveFileNames ? host.useCaseSensitiveFileNames() : false;
}

export function hostGetCanonicalFileName(host: LanguageServiceHost): GetCanonicalFileName {
return createGetCanonicalFileName(hostUsesCaseSensitiveFileNames(host));
}
}

// Display-part writer helpers
Expand Down
5 changes: 5 additions & 0 deletions tests/cases/fourslash/fourslash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,11 @@ declare namespace FourSlashInterface {
getSuggestionDiagnostics(expected: ReadonlyArray<Diagnostic>): void;
ProjectInfo(expected: string[]): void;
allRangesAppearInImplementationList(markerName: string): void;
updateFilePath(options: {
oldPath: string;
newPath: string;
newFileContents: { [fileName: string]: string };
});
}
class edit {
backspace(count?: number): void;
Expand Down
Loading

0 comments on commit 95525be

Please sign in to comment.