diff --git a/Jakefile.js b/Jakefile.js index fc2b6359355c5..27026e1323e06 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -164,7 +164,9 @@ var servicesSources = [ "formatting/rulesMap.ts", "formatting/rulesProvider.ts", "formatting/smartIndenter.ts", - "formatting/tokenRange.ts" + "formatting/tokenRange.ts", + "coderefactorings/codeRefactoringProvider.ts", + "coderefactorings/coderefactorings.ts", ].map(function (f) { return path.join(servicesDirectory, f); })); diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 9a353e6fb3651..c46a847b4c597 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3162,5 +3162,9 @@ "Adding a tsconfig.json file will help organize projects that contain both TypeScript and JavaScript files. Learn more at https://aka.ms/tsconfig": { "category": "Error", "code": 90009 + }, + "Inline temporary variable": { + "category": "Message", + "code": 91001 } } diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 6ff4dccdee30e..a8ca14e6cbc22 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -84,6 +84,11 @@ namespace FourSlash { end: number; } + export interface ExpectedFileChange { + fileName: string; + expectedText: string; + } + export import IndentStyle = ts.IndentStyle; const entityMap = ts.createMap({ @@ -478,7 +483,7 @@ namespace FourSlash { endPos = endMarker.position; } - errors.forEach(function(error: ts.Diagnostic) { + errors.forEach(function (error: ts.Diagnostic) { if (predicate(error.start, error.start + error.length, startPos, endPos)) { exists = true; } @@ -495,7 +500,7 @@ namespace FourSlash { Harness.IO.log("Unexpected error(s) found. Error list is:"); } - errors.forEach(function(error: ts.Diagnostic) { + errors.forEach(function (error: ts.Diagnostic) { Harness.IO.log(" minChar: " + error.start + ", limChar: " + (error.start + error.length) + ", message: " + ts.flattenDiagnosticMessageText(error.messageText, Harness.IO.newLine()) + "\n"); @@ -2046,6 +2051,51 @@ namespace FourSlash { } } + public verifyRefactoringAtPosition(expectedChanges: ExpectedFileChange[], refactoringId: string) { + // file the refactoring is triggered from + const sourceFileName = this.activeFile.fileName; + + const markers = this.getMarkers(); + if (markers.length < 1 || markers.length > 2) { + this.raiseError(`Expected 1 or 2 markers, actually found: ${markers.length}`); + } + const start = markers[0].position; + const end = markers[1] ? markers[1].position : markers[0].position; + + const textChanges = this.languageService.getChangesForCodeRefactoringAtPosition(sourceFileName, start, end, refactoringId, /*options*/ undefined, this.languageService); + if (!textChanges || textChanges.length == 0) { + this.raiseError("No code refactorings found."); + } + + // for each file: + // * optionally apply the changes + // * check if the new contents match the expected contents + // * if we applied changes, but don't check the content raise an error + ts.forEach(this.testData.files, file => { + const refactorForFile = ts.find(textChanges, change => { + return change.fileName == file.fileName; + }); + + if (refactorForFile) { + this.applyEdits(file.fileName, refactorForFile.textChanges, /*isFormattingEdit*/ false); + } + + const expectedFile = ts.find(expectedChanges, expected => { + const name = expected.fileName; + const fullName = name.indexOf("/") === -1 ? (this.basePath + "/" + name) : name; + return fullName === file.fileName; + }); + + if (refactorForFile && !expectedFile) { + this.raiseError(`Applied changes to '${file.fileName}' which was not expected.`); + } + const actualText = this.getFileContent(file.fileName); + if (this.removeWhitespace(expectedFile.expectedText) !== this.removeWhitespace(actualText)) { + this.raiseError(`Actual text doesn't match expected text. Actual: '${actualText}' Expected: '${expectedFile.expectedText}'`); + } + }); + } + public verifyDocCommentTemplate(expected?: ts.TextInsertion) { const name = "verifyDocCommentTemplate"; const actual = this.languageService.getDocCommentTemplateAtPosition(this.activeFile.fileName, this.currentCaretPosition); @@ -3303,6 +3353,10 @@ namespace FourSlashInterface { this.state.verifyCodeFixAtPosition(expectedText, errorCode); } + public inlineTempAtPosition(expectedChanges: FourSlash.ExpectedFileChange[]): void { + this.state.verifyRefactoringAtPosition(expectedChanges, ts.Diagnostics.Inline_temporary_variable.code.toString()); + } + public navigationBar(json: any) { this.state.verifyNavigationBar(json); } diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index a49f89267299f..7eba4adda44ce 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -1,4 +1,4 @@ -/// +/// /// /// /// @@ -126,7 +126,7 @@ namespace Harness.LanguageService { protected virtualFileSystem: Utils.VirtualFileSystem = new Utils.VirtualFileSystem(virtualFileSystemRoot, /*useCaseSensitiveFilenames*/false); constructor(protected cancellationToken = DefaultHostCancellationToken.Instance, - protected settings = ts.getDefaultCompilerOptions()) { + protected settings = ts.getDefaultCompilerOptions()) { } public getNewLine(): string { @@ -135,7 +135,7 @@ namespace Harness.LanguageService { public getFilenames(): string[] { const fileNames: string[] = []; - for (const virtualEntry of this.virtualFileSystem.getAllFileEntries()){ + for (const virtualEntry of this.virtualFileSystem.getAllFileEntries()) { const scriptInfo = virtualEntry.content; if (scriptInfo.isRootFile) { // only include root files here @@ -211,8 +211,8 @@ namespace Harness.LanguageService { readDirectory(path: string, extensions?: string[], exclude?: string[], include?: string[]): string[] { return ts.matchFiles(path, extensions, exclude, include, /*useCaseSensitiveFileNames*/false, - this.getCurrentDirectory(), - (p) => this.virtualFileSystem.getAccessibleFileSystemEntries(p)); + this.getCurrentDirectory(), + (p) => this.virtualFileSystem.getAccessibleFileSystemEntries(p)); } readFile(path: string): string { const snapshot = this.getScriptSnapshot(path); @@ -458,7 +458,6 @@ namespace Harness.LanguageService { getNavigationTree(fileName: string): ts.NavigationTree { return unwrapJSONCallResult(this.shim.getNavigationTree(fileName)); } - getOutliningSpans(fileName: string): ts.OutliningSpan[] { return unwrapJSONCallResult(this.shim.getOutliningSpans(fileName)); } @@ -487,7 +486,13 @@ namespace Harness.LanguageService { return unwrapJSONCallResult(this.shim.isValidBraceCompletionAtPosition(fileName, position, openingBrace)); } getCodeFixesAtPosition(): ts.CodeAction[] { - throw new Error("Not supported on the shim."); + throw new Error("getCodeFixesAtPosition not supported on the shim."); + } + getAvailableCodeRefactoringsAtPosition(): ts.CodeRefactoring[] { + throw new Error("getAvailableCodeRefactoringsAtPosition not supported on the shim."); + } + getChangesForCodeRefactoringAtPosition(): ts.FileTextChanges[] { + throw new Error("getChangesForCodeRefactoringAtPosition not supported on the shim."); } getEmitOutput(fileName: string): ts.EmitOutput { return unwrapJSONCallResult(this.shim.getEmitOutput(fileName)); diff --git a/src/server/client.ts b/src/server/client.ts index 3b09a92675497..2e857e109d0d0 100644 --- a/src/server/client.ts +++ b/src/server/client.ts @@ -1,4 +1,4 @@ -/// +/// namespace ts.server { export interface SessionClientHost extends LanguageServiceHost { @@ -419,7 +419,7 @@ namespace ts.server { } getSyntacticDiagnostics(fileName: string): Diagnostic[] { - const args: protocol.SyntacticDiagnosticsSyncRequestArgs = { file: fileName, includeLinePosition: true }; + const args: protocol.SyntacticDiagnosticsSyncRequestArgs = { file: fileName, includeLinePosition: true }; const request = this.processRequest(CommandNames.SyntacticDiagnosticsSync, args); const response = this.processResponse(request); @@ -690,17 +690,59 @@ namespace ts.server { return response.body.map(entry => this.convertCodeActions(entry, fileName)); } + + getAvailableCodeRefactoringsAtPosition(fileName: string, start: number, end: number, _serviceInstance: LanguageService): CodeRefactoring[] { + const startLineOffset = this.positionToOneBasedLineOffset(fileName, start); + const endLineOffset = this.positionToOneBasedLineOffset(fileName, end); + + const args: protocol.AvailableCodeRefactoringsRequestArgs = { + file: fileName, + startLine: startLineOffset.line, + startOffset: startLineOffset.offset, + endLine: endLineOffset.line, + endOffset: endLineOffset.offset, + }; + + const request = this.processRequest(CommandNames.GetCodeRefactorings, args); + const response = this.processResponse(request); + + return response.body; + } + + getChangesForCodeRefactoringAtPosition(fileName: string, start: number, end: number, refactoringId: string, options: any, _serviceInstance: LanguageService): FileTextChanges[] { + const startLineOffset = this.positionToOneBasedLineOffset(fileName, start); + const endLineOffset = this.positionToOneBasedLineOffset(fileName, end); + + const args: protocol.ApplyCodeRefactoringRequestArgs = { + file: fileName, + startLine: startLineOffset.line, + startOffset: startLineOffset.offset, + endLine: endLineOffset.line, + endOffset: endLineOffset.offset, + refactoringId: refactoringId, + input: options, + }; + + const request = this.processRequest(CommandNames.ApplyCodeRefactoring, args); + const response = this.processResponse(request); + + return response.body.map(entry => ({ + fileName: entry.fileName, + textChanges: entry.textChanges.map(codeEdit => this.convertCodeEditToTextChange(codeEdit, fileName)) + })); + } + convertCodeActions(entry: protocol.CodeAction, fileName: string): CodeAction { return { description: entry.description, changes: entry.changes.map(change => ({ fileName: change.fileName, - textChanges: change.textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, fileName)) + textChanges: change.textChanges.map(textChange => this.convertCodeEditToTextChange(textChange, fileName)) })) }; } - convertTextChangeToCodeEdit(change: protocol.CodeEdit, fileName: string): ts.TextChange { + convertCodeEditToTextChange(change: protocol.CodeEdit, fileName: string): ts.TextChange { const start = this.lineOffsetToPosition(fileName, change.start); const end = this.lineOffsetToPosition(fileName, change.end); diff --git a/src/server/protocol.ts b/src/server/protocol.ts index d13caf7f01b0c..dff9d99c994f7 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -95,6 +95,12 @@ namespace ts.server.protocol { /* @internal */ export type GetCodeFixesFull = "getCodeFixes-full"; export type GetSupportedCodeFixes = "getSupportedCodeFixes"; + export type GetCodeRefactorings = "getCodeRefactorings"; + /* @internal */ + export type GetCodeRefactoringsFull = "getCodeRefactorings-full"; + export type ApplyCodeRefactoring = "applyCodeRefactoring"; + /* @internal */ + export type ApplyCodeRefactoringFull = "applyCodeRefactoring-full"; } /** @@ -394,18 +400,7 @@ namespace ts.server.protocol { position?: number; } - /** - * Request for the available codefixes at a specific position. - */ - export interface CodeFixRequest extends Request { - command: CommandTypes.GetCodeFixes; - arguments: CodeFixRequestArgs; - } - - /** - * Instances of this interface specify errorcodes on a specific location in a sourcefile. - */ - export interface CodeFixRequestArgs extends FileRequestArgs { + export interface CodeChangeRequestArgs extends FileRequestArgs { /** * The line number for the request (1-based). */ @@ -437,7 +432,64 @@ namespace ts.server.protocol { */ /* @internal */ endPosition?: number; + } + + /** + * Request for the available code refactorings at a specific position. + */ + export interface AvailableCodeRefactoringsRequest extends Request { + command: CommandTypes.GetCodeRefactorings; + arguments: AvailableCodeRefactoringsRequestArgs; + } + + /** + * Response for GetCoderefactorings request. + */ + export interface AvailableCodeRefactoringResponse extends Response { + body?: CodeRefactoring[]; + } + + /** + * Instances of this interface request the available refactorings for a specific location in a sourcefile. + */ + export interface AvailableCodeRefactoringsRequestArgs extends CodeChangeRequestArgs { + + } + + /** + * Request to calculate the changes for a specific code refactoring at a specific position. + */ + export interface ApplyCodeRefactoringRequest extends Request { + command: CommandTypes.ApplyCodeRefactoring; + arguments: ApplyCodeRefactoringRequestArgs; + } + + export interface ApplyCodeRefactoringRequestArgs extends CodeChangeRequestArgs { + refactoringId: string; + input?: any; + } + + export interface ApplyCodeRefactoringResponse extends Response { + body?: FileCodeEdits[]; + } + /** + * Request for the available codefixes at a specific position. + */ + export interface CodeFixRequest extends Request { + command: CommandTypes.GetCodeFixes; + arguments: CodeFixRequestArgs; + } + + export interface CodeFixResponse extends Response { + /** The code actions that are available */ + body?: CodeAction[]; + } + + /** + * Instances of this interface specify errorcodes for a specific location in a sourcefile. + */ + export interface CodeFixRequestArgs extends CodeChangeRequestArgs { /** * Errorcodes we want to get the fixes for. */ @@ -1367,11 +1419,6 @@ namespace ts.server.protocol { textChanges: CodeEdit[]; } - export interface CodeFixResponse extends Response { - /** The code actions that are available */ - body?: CodeAction[]; - } - export interface CodeAction { /** Description of the code action to display in the UI of the editor */ description: string; diff --git a/src/server/session.ts b/src/server/session.ts index b250393b7ffdb..04b899d38e0de 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1,4 +1,4 @@ -/// +/// /// /// /// @@ -155,6 +155,10 @@ namespace ts.server { export const GetCodeFixes: protocol.CommandTypes.GetCodeFixes = "getCodeFixes"; export const GetCodeFixesFull: protocol.CommandTypes.GetCodeFixesFull = "getCodeFixes-full"; export const GetSupportedCodeFixes: protocol.CommandTypes.GetSupportedCodeFixes = "getSupportedCodeFixes"; + export const GetCodeRefactorings: protocol.CommandTypes.GetCodeRefactorings = "getCodeRefactorings"; + export const GetCodeRefactoringsFull: protocol.CommandTypes.GetCodeRefactoringsFull = "getCodeRefactorings-full"; + export const ApplyCodeRefactoring: protocol.CommandTypes.ApplyCodeRefactoring = "applyCodeRefactoring"; + export const ApplyCodeRefactoringFull: protocol.CommandTypes.ApplyCodeRefactoringFull = "applyCodeRefactoring-full"; } export function formatMessage(msg: T, logger: server.Logger, byteLength: (s: string, encoding: string) => number, newLine: string): string { @@ -1123,8 +1127,8 @@ namespace ts.server { return !items ? undefined : simplifiedResult - ? this.decorateNavigationBarItems(items, project.getScriptInfoForNormalizedPath(file)) - : items; + ? this.decorateNavigationBarItems(items, project.getScriptInfoForNormalizedPath(file)) + : items; } private decorateNavigationTree(tree: ts.NavigationTree, scriptInfo: ScriptInfo): protocol.NavigationTree { @@ -1150,8 +1154,8 @@ namespace ts.server { return !tree ? undefined : simplifiedResult - ? this.decorateNavigationTree(tree, project.getScriptInfoForNormalizedPath(file)) - : tree; + ? this.decorateNavigationTree(tree, project.getScriptInfoForNormalizedPath(file)) + : tree; } private getNavigateToItems(args: protocol.NavtoRequestArgs, simplifiedResult: boolean): protocol.NavtoItem[] | NavigateToItem[] { @@ -1265,6 +1269,62 @@ namespace ts.server { } } + private getAvailableCodeRefactorings(args: protocol.AvailableCodeRefactoringsRequestArgs): CodeRefactoring[] { + const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); + + const scriptInfo = project.getScriptInfoForNormalizedPath(file); + const startPosition = getStartPosition(); + const endPosition = getEndPosition(); + + const languageService = project.getLanguageService(); + const codeActions = languageService.getAvailableCodeRefactoringsAtPosition(file, startPosition, endPosition, languageService); + if (!codeActions) { + return undefined; + } + + return codeActions; + + function getStartPosition() { + return args.startPosition !== undefined ? args.startPosition : scriptInfo.lineOffsetToPosition(args.startLine, args.startOffset); + } + + function getEndPosition() { + return args.endPosition !== undefined ? args.endPosition : scriptInfo.lineOffsetToPosition(args.endLine, args.endOffset); + } + } + + private getChangesForRefactoring(args: protocol.ApplyCodeRefactoringRequestArgs, simplifiedResult: boolean): protocol.FileCodeEdits[] | FileTextChanges[] { + const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); + + const scriptInfo = project.getScriptInfoForNormalizedPath(file); + const startPosition = getStartPosition(); + const endPosition = getEndPosition(); + const languageService = project.getLanguageService(); + + const fileTextChanges = languageService.getChangesForCodeRefactoringAtPosition(file, startPosition, endPosition, args.refactoringId, args.input, languageService); + if (!fileTextChanges) { + return undefined; + } + + if (simplifiedResult) { + return fileTextChanges.map(fileTextChange => ({ + fileName: fileTextChange.fileName, + textChanges: fileTextChange.textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, scriptInfo)) + })); + } + else { + return fileTextChanges; + } + + function getStartPosition() { + return args.startPosition !== undefined ? args.startPosition : scriptInfo.lineOffsetToPosition(args.startLine, args.startOffset); + } + + function getEndPosition() { + return args.endPosition !== undefined ? args.endPosition : scriptInfo.lineOffsetToPosition(args.endLine, args.endOffset); + } + } + private mapCodeAction(codeAction: CodeAction, scriptInfo: ScriptInfo): protocol.CodeAction { return { description: codeAction.description, @@ -1293,8 +1353,8 @@ namespace ts.server { return !spans ? undefined : simplifiedResult - ? spans.map(span => this.decorateSpan(span, scriptInfo)) - : spans; + ? spans.map(span => this.decorateSpan(span, scriptInfo)) + : spans; } getDiagnosticsForProject(delay: number, fileName: string) { @@ -1597,7 +1657,19 @@ namespace ts.server { }, [CommandNames.GetSupportedCodeFixes]: () => { return this.requiredResponse(this.getSupportedCodeFixes()); - } + }, + [CommandNames.GetCodeRefactorings]: (request: protocol.AvailableCodeRefactoringsRequest) => { + return this.requiredResponse(this.getAvailableCodeRefactorings(request.arguments)); + }, + [CommandNames.GetCodeRefactoringsFull]: (request: protocol.AvailableCodeRefactoringsRequest) => { + return this.requiredResponse(this.getAvailableCodeRefactorings(request.arguments)); + }, + [CommandNames.ApplyCodeRefactoring]: (request: protocol.ApplyCodeRefactoringRequest) => { + return this.requiredResponse(this.getChangesForRefactoring(request.arguments, /*simplifiedResult*/ true)); + }, + [CommandNames.ApplyCodeRefactoringFull]: (request: protocol.ApplyCodeRefactoringRequest) => { + return this.requiredResponse(this.getChangesForRefactoring(request.arguments, /*simplifiedResult*/ false)); + }, }); public addProtocolHandler(command: string, handler: (request: protocol.Request) => { response?: any, responseRequired: boolean }) { diff --git a/src/server/utilities.ts b/src/server/utilities.ts index ac80965211923..56d5d8ad0106f 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -206,7 +206,9 @@ namespace ts.server { getCompletionEntrySymbol: throwLanguageServiceIsDisabledError, getImplementationAtPosition: throwLanguageServiceIsDisabledError, getSourceFile: throwLanguageServiceIsDisabledError, - getCodeFixesAtPosition: throwLanguageServiceIsDisabledError + getCodeFixesAtPosition: throwLanguageServiceIsDisabledError, + getAvailableCodeRefactoringsAtPosition: throwLanguageServiceIsDisabledError, + getChangesForCodeRefactoringAtPosition: throwLanguageServiceIsDisabledError, }; export interface ServerLanguageServiceHost { diff --git a/src/services/codefixes/codeFixProvider.ts b/src/services/codefixes/codeFixProvider.ts index c61cbe1b19ea5..541b0efdadb7e 100644 --- a/src/services/codefixes/codeFixProvider.ts +++ b/src/services/codefixes/codeFixProvider.ts @@ -5,14 +5,17 @@ namespace ts { getCodeActions(context: CodeFixContext): CodeAction[] | undefined; } - export interface CodeFixContext { - errorCode: number; + export interface CodeChangeContext { sourceFile: SourceFile; span: TextSpan; program: Program; newLineCharacter: string; } + export interface CodeFixContext extends CodeChangeContext { + errorCode: number; + } + export namespace codefix { const codeFixes = createMap(); diff --git a/src/services/coderefactorings/codeRefactoringProvider.ts b/src/services/coderefactorings/codeRefactoringProvider.ts new file mode 100644 index 0000000000000..19cd76a439920 --- /dev/null +++ b/src/services/coderefactorings/codeRefactoringProvider.ts @@ -0,0 +1,101 @@ +/* @internal */ +namespace ts { + export interface CodeRefactoringFactory { + refactoringId: string; + getAvailableRefactorings(context: CodeRefactoringContext): CodeRefactoring[]; + getChangesForRefactoring(context: CodeRefactoringContext): FileTextChanges[]; + } + + export interface CodeRefactoringContext extends CodeChangeContext { + languageService: LanguageService; + refactoringId?: string; + userInput?: any; + } + + export namespace coderefactoring { + const refactorings = createMap(); + + export function registerCodeRefactoringFactory(refactoring: CodeRefactoringFactory) { + const existing = refactorings[refactoring.refactoringId]; + if (!existing) { + refactorings[refactoring.refactoringId] = refactoring; + } + else { + Debug.fail("Trying to add a duplicate refactoring."); + } + } + + export function getAvailableCodeRefactorings(context: CodeRefactoringContext): CodeRefactoring[] { + let allRefactorings: CodeRefactoring[] = []; + + for (const key in refactorings) { + const factory = refactorings[key]; + const current = factory.getAvailableRefactorings(context); + if (current && current.length > 0) { + allRefactorings = allRefactorings.concat(current); + } + } + return allRefactorings; + } + + export function getTextChangesForRefactoring(context: CodeRefactoringContext): FileTextChanges[] { + const factory = refactorings[context.refactoringId]; + return factory.getChangesForRefactoring(context); + } + + /* @interal */ + function findEntry(fileName: string, fileTextChanges: FileTextChanges[]): FileTextChanges | undefined { + if (fileTextChanges && fileTextChanges.length > 0) { + for (const entry of fileTextChanges) { + if (entry.fileName === fileName) { + return entry; + } + } + } + return undefined; + } + + /* @interal */ + export function getOrCreateFileTextChangesEntry(reference: ReferenceEntry, fileTextChanges: FileTextChanges[]): FileTextChanges { + let fileTextChangesEntry = findEntry(reference.fileName, fileTextChanges); + if (!fileTextChangesEntry) { + fileTextChangesEntry = { + fileName: reference.fileName, + textChanges: [] + }; + fileTextChanges.push(fileTextChangesEntry); + } + return fileTextChangesEntry; + } + + /* @interal */ + export function getOrCreateFileTextChangesEntryFileName(fileName: string, fileTextChanges: FileTextChanges[]): FileTextChanges { + let fileTextChangesEntry = findEntry(fileName, fileTextChanges); + if (!fileTextChangesEntry) { + fileTextChangesEntry = { + fileName: fileName, + textChanges: [] + }; + fileTextChanges.push(fileTextChangesEntry); + } + return fileTextChangesEntry; + } + + /* @interal */ + export function isNodeOnLeft(node: Node, binaryExpression: BinaryExpression): boolean { + let posToCompare: number = -1; + for (let i = 0, n = binaryExpression.getChildCount(); i < n; i++) { + const child = binaryExpression.getChildAt(i); + if (child.kind === SyntaxKind.FirstAssignment) { + posToCompare = child.pos; + } + } + if (posToCompare != -1) { + if (node.pos < posToCompare) { + return true; + } + } + return false; + } + } +} diff --git a/src/services/coderefactorings/coderefactorings.ts b/src/services/coderefactorings/coderefactorings.ts new file mode 100644 index 0000000000000..3915ea3fcc93f --- /dev/null +++ b/src/services/coderefactorings/coderefactorings.ts @@ -0,0 +1 @@ +/// diff --git a/src/services/coderefactorings/inlineTempRefactor.ts b/src/services/coderefactorings/inlineTempRefactor.ts new file mode 100644 index 0000000000000..8b94da2580c39 --- /dev/null +++ b/src/services/coderefactorings/inlineTempRefactor.ts @@ -0,0 +1,182 @@ +/* @internal */ +namespace ts.coderefactoring { + registerCodeRefactoringFactory({ + refactoringId: Diagnostics.Inline_temporary_variable.code.toString(), + getAvailableRefactorings: (context: CodeRefactoringContext): CodeRefactoring[] => { + const sourceFile = context.sourceFile; + const identifier = getTokenAtPosition(sourceFile, context.span.start); + let variableDeclaration: VariableDeclaration; + if (identifier.kind !== SyntaxKind.Identifier || !(isVariableDeclaration(identifier.parent))) { + // this refectoring only works on a variable declarations + return undefined; + } + else { + variableDeclaration = identifier.parent; + } + + const namePos: number = variableDeclaration.name.pos; + const referenceSymbols: ReferencedSymbol[] = context.languageService.findReferences(context.sourceFile.fileName, namePos + 1); + + // If there are no referenced symbols this refactoring shouldn't + // do anything + if (!referenceSymbols || referenceSymbols.length === 0) { + return undefined; + } + + return [{ + description: getLocaleSpecificMessage(Diagnostics.Inline_temporary_variable), + refactoringId: Diagnostics.Inline_temporary_variable.code.toString() + }]; + }, + getChangesForRefactoring: (context: CodeRefactoringContext): FileTextChanges[] => { + const sourceFile = context.sourceFile; + const identifier = getTokenAtPosition(sourceFile, context.span.start); + let variableDeclaration: VariableDeclaration; + if (identifier.kind !== SyntaxKind.Identifier || !(isVariableDeclaration(identifier.parent))) { + // this refectoring only works on a variable declarations + return undefined; + } + else { + variableDeclaration = identifier.parent; + } + + const fileTextChanges: FileTextChanges[] = []; + const namePos: number = variableDeclaration.name.pos; + let variableInitializerText: string = variableDeclaration.initializer.getText(); + const program = context.program; + const referenceSymbols: ReferencedSymbol[] = context.languageService.findReferences(context.sourceFile.fileName, namePos + 1); + + // If there are no referenced symbols this refactoring shouldn't + // do anything + if (!referenceSymbols || referenceSymbols.length === 0) { + return undefined; + } + for (const symbol of referenceSymbols) { + for (const reference of symbol.references) { + if (!reference.isDefinition) { + const fileTextChangesEntry = getOrCreateFileTextChangesEntry(reference, fileTextChanges); + const node: Node = getTouchingPropertyName(program.getSourceFile(reference.fileName), reference.textSpan.start); + + if (node.kind === SyntaxKind.Identifier) { + if (node.parent.kind === SyntaxKind.BinaryExpression) { + const binaryExpression: BinaryExpression = node.parent; + if (isNodeOnLeft(node, binaryExpression)) { + variableInitializerText = binaryExpression.right.getText(); + handleBinaryExpression(binaryExpression, fileTextChangesEntry); + } + else { + fileTextChangesEntry.textChanges.push({ + newText: "(" + variableInitializerText + ")", + span: { + start: node.pos, + length: node.end - node.pos + } + }); + } + } + else if (node.parent.kind === SyntaxKind.PropertyAccessExpression || node.parent.kind === SyntaxKind.CallExpression || node.parent.kind === SyntaxKind.VariableDeclaration) { + fileTextChangesEntry.textChanges.push({ + newText: "(" + variableInitializerText + ")", + span: { + start: node.pos, + length: node.end - node.pos + } + }); + } + } + } + } + + if (variableDeclaration.parent.kind === SyntaxKind.VariableDeclarationList) { + const variableDeclarationList: VariableDeclarationList = variableDeclaration.parent; + const fileTextChangesEntry = getOrCreateFileTextChangesEntryFileName(context.sourceFile.fileName, fileTextChanges); + let startPos: number = -1; + let length: number = -1; + + if (variableDeclarationList.declarations.length === 1) { + // There is only declaration. The whole list could be removed. + startPos = variableDeclarationList.parent.pos; + length = variableDeclarationList.parent.end - variableDeclarationList.parent.pos; + } + else { + if (variableDeclarationList.declarations[0] === variableDeclaration) { + // It is the first declaration. So, the following comma also must be removed + startPos = variableDeclaration.pos; + length = variableDeclaration.end - variableDeclaration.pos + 1; + } + else { + startPos = variableDeclaration.pos - 1; + length = variableDeclaration.end - variableDeclaration.pos + 1; + } + } + + fileTextChangesEntry.textChanges.push({ + newText: "", + span: { + start: startPos, + length: length + } + }); + } + + return fileTextChanges; + } + } + }); + + function isVariableDeclaration(token: Node): token is VariableDeclaration { + return token.kind === SyntaxKind.VariableDeclaration; + } + + function handleBinaryExpression(binaryExpression: BinaryExpression, fileTextChangesEntry: FileTextChanges) { + let startPos: number = -1, length: number = -1; + + if (binaryExpression.parent.kind === SyntaxKind.ExpressionStatement) { + startPos = binaryExpression.parent.pos; + length = binaryExpression.parent.end - binaryExpression.parent.pos; + } + else if (binaryExpression.parent.kind === SyntaxKind.BinaryExpression) { + const parentBinaryExpression: BinaryExpression = binaryExpression.parent; + if (parentBinaryExpression.left === binaryExpression) { + startPos = binaryExpression.pos; + length = binaryExpression.end - binaryExpression.pos + 1; + } + else { + startPos = binaryExpression.pos - 1; + length = binaryExpression.end - binaryExpression.pos + 1; + } + } + + fileTextChangesEntry.textChanges.push({ + newText: "", + span: { + start: startPos, + length: length + } + }); + } + + export function getTokenAtRange(sourceFile: SourceFile, start: number, end: number): Node { + return findTokenAtRange(sourceFile, start, end, sourceFile); + } + + function findTokenAtRange(current: Node, start: number, end: number, sourceFile: SourceFile) { + let resultNode: Node = undefined; + for (let i = 0, n = current.getChildCount(sourceFile); i < n; i++) { + const child = current.getChildAt(i); + const startPos = child.getStart(sourceFile, true); + const endPos = child.getEnd(); + if (startPos > end) { // means the control is at a token well beyond the range + break; + } + if (startPos === start && endPos === end) { + resultNode = child; + } + if (child.getChildCount(sourceFile) > 0) { + const tempResultNode = findTokenAtRange(child, start, end, sourceFile); + resultNode = (tempResultNode) ? tempResultNode : resultNode; + } + } + return resultNode; + } +} diff --git a/src/services/services.ts b/src/services/services.ts index 56e604abeb3eb..50cb74cd322d1 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1,4 +1,4 @@ -/// +/// /// /// @@ -26,6 +26,8 @@ /// /// /// +/// +/// namespace ts { /** The version of the language service API */ @@ -1688,6 +1690,47 @@ namespace ts { return allFixes; } + function getAvailableCodeRefactoringsAtPosition(fileName: string, start: number, end: number, serviceInstance: LanguageService): CodeRefactoring[] { + synchronizeHostData(); + const sourceFile = getValidSourceFile(fileName); + const span = { start, length: end - start }; + const newLineChar = getNewLineOrDefaultFromHost(host); + + cancellationToken.throwIfCancellationRequested(); + + const context = { + sourceFile: sourceFile, + span: span, + program: program, + newLineCharacter: newLineChar, + languageService: serviceInstance + }; + + return coderefactoring.getAvailableCodeRefactorings(context); + } + + function getChangesForCodeRefactoringAtPosition(fileName: string, start: number, end: number, refactoringId: string, options: any, serviceInstance: LanguageService): FileTextChanges[] { + synchronizeHostData(); + const sourceFile = getValidSourceFile(fileName); + const span = { start, length: end - start }; + const newLineChar = getNewLineOrDefaultFromHost(host); + + cancellationToken.throwIfCancellationRequested(); + + const context = { + sourceFile: sourceFile, + span: span, + refactoringId, + options, + program: program, + newLineCharacter: newLineChar, + languageService: serviceInstance, + }; + + return coderefactoring.getTextChangesForRefactoring(context); + } + + function getDocCommentTemplateAtPosition(fileName: string, position: number): TextInsertion { return JsDoc.getDocCommentTemplateAtPosition(getNewLineOrDefaultFromHost(host), syntaxTreeCache.getCurrentSourceFile(fileName), position); } @@ -1913,6 +1956,8 @@ namespace ts { getDocCommentTemplateAtPosition, isValidBraceCompletionAtPosition, getCodeFixesAtPosition, + getAvailableCodeRefactoringsAtPosition, + getChangesForCodeRefactoringAtPosition, getEmitOutput, getNonBoundSourceFile, getSourceFile, diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index a585236c6662e..bd25ea34e9226 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -85,7 +85,9 @@ "formatting/rulesProvider.ts", "formatting/smartIndenter.ts", "formatting/tokenRange.ts", - "codeFixes/codeFixProvider.ts", - "codeFixes/fixes.ts" + "codefixes/codeFixProvider.ts", + "codefixes/fixes.ts", + "coderefactorings/codeRefactoringProvider.ts", + "coderefactorings/coderefactorings.ts" ] } diff --git a/src/services/types.ts b/src/services/types.ts index 4e04df3fc7caf..4d9eef4791c87 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -1,4 +1,4 @@ -namespace ts { +namespace ts { export interface Node { getSourceFile(): SourceFile; getChildCount(sourceFile?: SourceFile): number; @@ -242,11 +242,16 @@ namespace ts { getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: number[]): CodeAction[]; + getAvailableCodeRefactoringsAtPosition(fileName: string, start: number, end: number, serviceInstance: LanguageService): CodeRefactoring[]; + + getChangesForCodeRefactoringAtPosition(fileName: string, start: number, end: number, refactoringId: string, options: any, serviceInstance: LanguageService): FileTextChanges[]; + getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput; getProgram(): Program; - /* @internal */ getNonBoundSourceFile(fileName: string): SourceFile; + /* @internal */ + getNonBoundSourceFile(fileName: string): SourceFile; /** * @internal @@ -320,6 +325,16 @@ namespace ts { newText: string; } + export interface CodeRefactoring { + /** Description of the code refactoring to display in the UI of the editor */ + description: string; + /** The unique Id for the refactoring so we can invoke it when requested + * by the user. */ + refactoringId: string; + /** Template or default input for the refactoring */ + defaultUserInput?: any; + } + export interface FileTextChanges { fileName: string; textChanges: TextChange[]; diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 295b8e422b9ba..1e05f4acdc704 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -117,6 +117,10 @@ declare namespace FourSlashInterface { file(index: number, content?: string, scriptKindName?: string): any; file(name: string, content?: string, scriptKindName?: string): any; } + interface ExpectedFileChange { + fileName: string; + expectedText: string; + } class verifyNegatable { private negative; not: verifyNegatable; @@ -211,7 +215,7 @@ declare namespace FourSlashInterface { DocCommentTemplate(expectedText: string, expectedOffset: number, empty?: boolean): void; noDocCommentTemplate(): void; codeFixAtPosition(expectedText: string, errorCode?: number): void; - + inlineTempAtPosition(expectedChanges: ExpectedFileChange[]): void; navigationBar(json: any): void; navigationTree(json: any): void; navigationItemsListCount(count: number, searchValue: string, matchKind?: string, fileName?: string): void; diff --git a/tests/cases/fourslash/inlineTemp1.ts b/tests/cases/fourslash/inlineTemp1.ts new file mode 100644 index 0000000000000..aa135f0e0de5d --- /dev/null +++ b/tests/cases/fourslash/inlineTemp1.ts @@ -0,0 +1,20 @@ +/// + +// @Filename: file1.ts +//// export module m1 { +//// function f1() { +//// let /*0*/s1:string ="dummy"; +//// let m1: string = "dummy2", m: string = "dummy", m3: string = "dummy3" ; +//// let s2: string = s1.replace(s1, "") + m + "some value" + s1; +//// } +//// } + +verify.inlineTempAtPosition([{ + fileName: "file1.ts", + expectedText: ` +export module m1 { + function f1() { + let m1: string = "dummy2", m: string = "dummy", m3: string = "dummy3" ; + let s2: string = ("dummy").replace(("dummy"), "") + m + "some value" + ("dummy"); + } +}`}]); \ No newline at end of file diff --git a/tests/cases/fourslash/inlineTemp2.ts b/tests/cases/fourslash/inlineTemp2.ts new file mode 100644 index 0000000000000..af38c797edf11 --- /dev/null +++ b/tests/cases/fourslash/inlineTemp2.ts @@ -0,0 +1,26 @@ +/// + +// @Filename: file1.ts +//// export module m1 { +//// function f1() { +//// let s3:string = "mmmmm", j:number = 98; +//// let /*0*/s1:string ="dummy"; +//// j = 87, s3="rrrr", s1 = "ddddd"; +//// let m1: string = "dummy2", m: string = "dummy", m3: string = s1; +//// s1 = "kkk"; +//// let s2: string = s1.replace(s1, "") + m + "some value" + s1; +//// } +//// } + +verify.inlineTempAtPosition([{ + fileName: "file1.ts", + expectedText: ` +export module m1 { + function f1() { + let s3:string = "mmmmm", j:number = 98; + j = 87, s3="rrrr"; + let m1: string = "dummy2", m: string = "dummy", m3: string = ("ddddd") ; + let s2: string = ("kkk").replace(("kkk"), "") + m + "some value" + ("kkk"); + } +} +`}]); \ No newline at end of file diff --git a/tests/cases/fourslash/inlineTemp3.ts b/tests/cases/fourslash/inlineTemp3.ts new file mode 100644 index 0000000000000..0a2c0832c6886 --- /dev/null +++ b/tests/cases/fourslash/inlineTemp3.ts @@ -0,0 +1,26 @@ +/// + +// @Filename: file1.ts +//// export module m1 { +//// function f1() { +//// let s3:string = "mmmmm", j:number = 98; +//// let /*0*/s1:string ="dummy"; +//// s1 = "ddddd", j = 87, s3="rrrr"; +//// let m1: string = "dummy2", m: string = "dummy", m3: string = s1; +//// s1 = "kkk"; +//// let s2: string = s1.replace(s1, "") + m + "some value" + s1; +//// } +//// } + +verify.inlineTempAtPosition([{ + fileName: "file1.ts", + expectedText: ` +export module m1 { + function f1() { + let s3:string = "mmmmm", j:number = 98; + j = 87, s3="rrrr"; + let m1: string = "dummy2", m: string = "dummy", m3: string = ("ddddd") ; + let s2: string = ("kkk").replace(("kkk"), "") + m + "some value" + ("kkk"); + } +} +`}]); \ No newline at end of file diff --git a/tests/cases/fourslash/inlineTemp4.ts b/tests/cases/fourslash/inlineTemp4.ts new file mode 100644 index 0000000000000..04ec5d8972310 --- /dev/null +++ b/tests/cases/fourslash/inlineTemp4.ts @@ -0,0 +1,26 @@ +/// + +// @Filename: file1.ts +//// export module m1 { +//// function f1() { +//// let s3:string = "mmmmm", j:number = 98; +//// let /*0*/s1:string ="dummy"; +//// j = 87, s1 = "ddddd", s3="rrrr"; +//// let m1: string = "dummy2", m: string = "dummy", m3: string = s1; +//// s1 = "kkk"; +//// let s2: string = s1.replace(s1, "") + m + "some value" + s1; +//// } +//// } + +verify.inlineTempAtPosition([{ + fileName: "file1.ts", + expectedText: ` +export module m1 { + function f1() { + let s3:string = "mmmmm", j:number = 98; + j = 87, s3="rrrr"; + let m1: string = "dummy2", m: string = "dummy", m3: string = ("ddddd") ; + let s2: string = ("kkk").replace(("kkk"), "") + m + "some value" + ("kkk"); + } +} +`}]); \ No newline at end of file diff --git a/tests/cases/fourslash/inlineTemp5.ts b/tests/cases/fourslash/inlineTemp5.ts new file mode 100644 index 0000000000000..fe2b15834bd85 --- /dev/null +++ b/tests/cases/fourslash/inlineTemp5.ts @@ -0,0 +1,27 @@ +/// + +// @Filename: file1.ts +//// export module m1 { +//// function f1() { +//// let j:number = 98; +//// let /*0*/s1:string ="dummy", s3:string = "mmmmm"; +//// j = 87, s1 = "ddddd", s3="rrrr"; +//// let m1: string = "dummy2", m: string = "dummy", m3: string = s1; +//// s1 = "kkk"; +//// let s2: string = s1.replace(s1, "") + m + "some value" + s1; +//// } +//// } + +verify.inlineTempAtPosition([{ + fileName: "file1.ts", + expectedText: ` +export module m1 { + function f1() { + let j:number = 98; + let s3:string = "mmmmm"; + j = 87, s3="rrrr"; + let m1: string = "dummy2", m: string = "dummy", m3: string = ("ddddd") ; + let s2: string = ("kkk").replace(("kkk"), "") + m + "some value" + ("kkk"); + } +} +`}]); \ No newline at end of file diff --git a/tests/cases/fourslash/inlineTemp6.ts b/tests/cases/fourslash/inlineTemp6.ts new file mode 100644 index 0000000000000..9e83afeb408ca --- /dev/null +++ b/tests/cases/fourslash/inlineTemp6.ts @@ -0,0 +1,29 @@ +/// + +// @Filename: file1.ts +//// export module m1 { +//// function f1() { +//// let j:number = 98; +//// let /*0*/s1:string ="dummy", s3:string = "mmmmm"; +//// let r1 = s1.replace("d", "g"); +//// j = 87, s1 = "ddddd", s3="rrrr"; +//// let m1: string = "dummy2", m: string = "dummy", m3: string = s1; +//// s1 = "kkk"; +//// let s2: string = s1.replace(s1, "") + m + "some value" + s1; +//// } +//// } + +verify.inlineTempAtPosition([{ + fileName: "file1.ts", + expectedText: ` +export module m1 { + function f1() { + let j:number = 98; + let s3:string = "mmmmm"; + let r1 = ("dummy").replace("d", "g"); + j = 87, s3="rrrr"; + let m1: string = "dummy2", m: string = "dummy", m3: string = ("ddddd") ; + let s2: string = ("kkk").replace(("kkk"), "") + m + "some value" + ("kkk"); + } +} +`}]); \ No newline at end of file