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