Skip to content

Pvb/refactorapi #11913

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 9 commits into from
4 changes: 3 additions & 1 deletion Jakefile.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}));
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
58 changes: 56 additions & 2 deletions src/harness/fourslash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ namespace FourSlash {
end: number;
}

export interface ExpectedFileChange {
fileName: string;
expectedText: string;
}

export import IndentStyle = ts.IndentStyle;

const entityMap = ts.createMap({
Expand Down Expand Up @@ -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;
}
Expand All @@ -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");
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
Expand Down
19 changes: 12 additions & 7 deletions src/harness/harnessLanguageService.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/// <reference path="..\services\services.ts" />
/// <reference path="..\services\services.ts" />
/// <reference path="..\services\shims.ts" />
/// <reference path="..\server\client.ts" />
/// <reference path="harness.ts" />
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -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));
Expand Down
50 changes: 46 additions & 4 deletions src/server/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/// <reference path="session.ts" />
/// <reference path="session.ts" />

namespace ts.server {
export interface SessionClientHost extends LanguageServiceHost {
Expand Down Expand Up @@ -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<protocol.SyntacticDiagnosticsSyncRequest>(CommandNames.SyntacticDiagnosticsSync, args);
const response = this.processResponse<protocol.SyntacticDiagnosticsSyncResponse>(request);
Expand Down Expand Up @@ -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<protocol.AvailableCodeRefactoringsRequest>(CommandNames.GetCodeRefactorings, args);
const response = this.processResponse<protocol.AvailableCodeRefactoringResponse>(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<protocol.ApplyCodeRefactoringRequest>(CommandNames.ApplyCodeRefactoring, args);
const response = this.processResponse<protocol.ApplyCodeRefactoringResponse>(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);

Expand Down
81 changes: 64 additions & 17 deletions src/server/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}

/**
Expand Down Expand Up @@ -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).
*/
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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;
Expand Down
Loading