Skip to content

Commit

Permalink
Redesign APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
zhengbli committed Mar 26, 2017
1 parent fa5a483 commit 132051b
Show file tree
Hide file tree
Showing 10 changed files with 315 additions and 236 deletions.
4 changes: 4 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -3362,5 +3362,9 @@
"Octal literals are not allowed in enums members initializer. Use the syntax '{0}'.": {
"category": "Error",
"code": 8018
},
"Convert function '{0}' to ES6 class.": {
"category": "Refactor",
"code": 100000
}
}
1 change: 1 addition & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3301,6 +3301,7 @@ namespace ts {
Warning,
Error,
Message,
Refactor
}

export enum ModuleResolutionKind {
Expand Down
127 changes: 80 additions & 47 deletions src/harness/fourslash.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
Expand Down Expand Up @@ -489,8 +490,8 @@ namespace FourSlash {
return diagnostics;
}

private getRefactorDiagnostics(fileName: string, range?: ts.TextRange): ts.RefactorDiagnostic[] {
return this.languageService.getRefactorDiagnostics(fileName, range);
private getRefactorDiagnostics(fileName: string): ts.Diagnostic[] {
return this.languageService.getRefactorDiagnostics(fileName);
}

private getAllDiagnostics(): ts.Diagnostic[] {
Expand Down Expand Up @@ -2200,20 +2201,15 @@ namespace FourSlash {
return actions;
}

private getRefactorActions(fileName: string, range?: ts.TextRange, formattingOptions?: ts.FormatCodeSettings): ts.CodeAction[] {
const diagnostics = this.getRefactorDiagnostics(fileName, range);
const actions: ts.CodeAction[] = [];
formattingOptions = formattingOptions || this.formatCodeSettings;
private applyAllCodeActions(fileName: string, codeActions: ts.CodeAction[]): void {
if (!codeActions) {
return;
}

for (const diagnostic of diagnostics) {
const diagnosticRange: ts.TextRange = {
pos: diagnostic.start,
end: diagnostic.end
};
const newActions = this.languageService.getCodeActionsForRefactorAtPosition(fileName, diagnosticRange, diagnostic.code, formattingOptions);
actions.push.apply(actions, newActions);
for (const codeAction of codeActions) {
const fileChanges = ts.find(codeAction.changes, change => change.fileName === fileName);
this.applyEdits(fileChanges.fileName, fileChanges.textChanges, /*isFormattingEdit*/ false);
}
return actions;
}

private applyCodeAction(fileName: string, actions: ts.CodeAction[], index?: number): void {
Expand Down Expand Up @@ -2574,42 +2570,79 @@ namespace FourSlash {
}
}

public verifyRefactorAvailable(negative: boolean) {
// The ranges are used only when the refactors require a range as input information. For example the "extractMethod" refactor
// onlye one range is allowed per test
const ranges = this.getRanges();
if (ranges.length > 1) {
throw new Error("only one refactor range is allowed per test");
public verifyRefactorDiagnosticsAvailableAtMarker(negative: boolean, markerName: string, diagnosticCode?: number) {
const marker = this.getMarkerByName(markerName);
const markerPos = marker.position;
let foundDiagnostic = false;

const refactorDiagnostics = this.getRefactorDiagnostics(this.activeFile.fileName);
for (const diag of refactorDiagnostics) {
if (diag.start <= markerPos && diag.start + diag.length >= markerPos) {
foundDiagnostic = diagnosticCode === undefined || diagnosticCode === diag.code;
}
}

if (negative && foundDiagnostic) {
this.raiseError(`verifyRefactorDiagnosticsAvailableAtMarker failed - expected no refactor diagnostic at marker ${markerName} but found some.`);
}
if (!negative && !foundDiagnostic) {
this.raiseError(`verifyRefactorDiagnosticsAvailableAtMarker failed - expected a refactor diagnostic at marker ${markerName} but found none.`);
}
}

const range = ranges[0] ? { pos: ranges[0].start, end: ranges[0].end } : undefined;
const refactorDiagnostics = this.getRefactorDiagnostics(this.activeFile.fileName, range);
if (negative && refactorDiagnostics.length > 0) {
this.raiseError(`verifyRefactorAvailable failed - expected no refactors but found some.`);
public verifyApplicableRefactorAvailableAtMarker(negative: boolean, markerName: string) {
const marker = this.getMarkerByName(markerName);
const applicableRefactors = this.languageService.getApplicableRefactors(this.activeFile.fileName, marker.position);
const isAvailable = applicableRefactors && applicableRefactors.length > 0;
if (negative && isAvailable) {
this.raiseError(`verifyApplicableRefactorAvailableAtMarker failed - expected no refactor at marker ${markerName} but found some.`);
}
if (!negative && refactorDiagnostics.length === 0) {
this.raiseError(`verifyRefactorAvailable failed: expected refactor diagnostics but none found.`);
if (!negative && !isAvailable) {
this.raiseError(`verifyApplicableRefactorAvailableAtMarker failed - expected a refactor at marker ${markerName} but found none.`);
}
}

public verifyFileAfterApplyingRefactors(expectedContent: string, formattingOptions?: ts.FormatCodeSettings) {
public verifyApplicableRefactorAvailableForRange(negative: boolean) {
const ranges = this.getRanges();
if (ranges.length > 1) {
throw new Error("only one refactor range is allowed per test");
}

const range = ranges[0] ? { pos: ranges[0].start, end: ranges[0].end } : undefined;
const actions = this.getRefactorActions(this.activeFile.fileName, range, formattingOptions);

// Each refactor diagnostic will return one code action, but multiple refactor diagnostics can point to the same
// code action. For example in the case of "convert function to es6 class":
//
// function foo() { }
// ^^^
// foo.prototype.getName = function () { return "name"; };
// ^^^
// These two diagnostics result in the same code action, so we only apply the first one.
this.applyCodeAction(this.activeFile.fileName, actions, /*index*/ 0);
if (!(ranges && ranges.length === 1)) {
throw new Error("Exactly one refactor range is allowed per test.");
}

const applicableRefactors = this.languageService.getApplicableRefactors(this.activeFile.fileName, { pos: ranges[0].start, end: ranges[0].end });
const isAvailable = applicableRefactors && applicableRefactors.length > 0;
if (negative && isAvailable) {
this.raiseError(`verifyApplicableRefactorAvailableForRange failed - expected no refactor but found some.`);
}
if (!negative && !isAvailable) {
this.raiseError(`verifyApplicableRefactorAvailableForRange failed - expected a refactor but found none.`);
}
}

public verifyFileAfterApplyingRefactorAtMarker(
markerName: string,
expectedContent: string,
refactorKindToApply?: ts.RefactorKind,
formattingOptions?: ts.FormatCodeSettings) {

formattingOptions = formattingOptions || this.formatCodeSettings;
const markerPos = this.getMarkerByName(markerName).position;
const diagnostics = this.getRefactorDiagnostics(this.activeFile.fileName);

const diagnosticCodesAtMarker: number[] = [];
for (const diagnostic of diagnostics) {
if (diagnostic.start <= markerPos && diagnostic.start + diagnostic.length >= markerPos) {
diagnosticCodesAtMarker.push(diagnostic.code);
}
}

const applicableRefactors = this.languageService.getApplicableRefactors(this.activeFile.fileName, markerPos);
const applicableRefactorKinds =
ts.map(ts.filter(applicableRefactors, ar => refactorKindToApply === undefined || refactorKindToApply === ar.refactorKind),
refactorInfo => refactorInfo.refactorKind);
const codeActions = this.languageService.getRefactorCodeActions(
this.activeFile.fileName, formattingOptions, markerPos, applicableRefactorKinds, diagnosticCodesAtMarker);

this.applyAllCodeActions(this.activeFile.fileName, codeActions);
const actualContent = this.getFileContent(this.activeFile.fileName);

if (this.normalizeNewlines(actualContent) !== this.normalizeNewlines(expectedContent)) {
Expand Down Expand Up @@ -3409,8 +3442,8 @@ namespace FourSlashInterface {
this.state.verifyCodeFixAvailable(this.negative);
}

public refactorAvailable() {
this.state.verifyRefactorAvailable(this.negative);
public refactorAvailable(negative: boolean, markerName: string, refactorCode?: number) {
this.state.verifyRefactorDiagnosticsAvailableAtMarker(negative, markerName, refactorCode);
}
}

Expand Down Expand Up @@ -3618,8 +3651,8 @@ namespace FourSlashInterface {
this.state.verifyRangeAfterCodeFix(expectedText, includeWhiteSpace, errorCode, index);
}

public fileAfterApplyingRefactors(expectedContent: string, formattingOptions: ts.FormatCodeSettings): void {
this.state.verifyFileAfterApplyingRefactors(expectedContent, formattingOptions);
public fileAfterApplyingRefactorsAtMarker(markerName: string, expectedContent: string, refactorKindToApply?: ts.RefactorKind, formattingOptions?: ts.FormatCodeSettings): void {
this.state.verifyFileAfterApplyingRefactorAtMarker(markerName, expectedContent, refactorKindToApply, formattingOptions);
}

public importFixAtPosition(expectedTextArray: string[], errorCode?: number): void {
Expand Down
7 changes: 5 additions & 2 deletions src/harness/harnessLanguageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,10 +489,13 @@ namespace Harness.LanguageService {
getCodeFixesAtPosition(): ts.CodeAction[] {
throw new Error("Not supported on the shim.");
}
getRefactorDiagnostics(): ts.RefactorDiagnostic[] {
getRefactorDiagnostics(): ts.Diagnostic[] {
throw new Error("Not supported on the shim.");
}
getCodeActionsForRefactorAtPosition(): ts.CodeAction[] {
getRefactorCodeActions(): ts.CodeAction[] {
throw new Error("Not supported on the shim.");
}
getApplicableRefactors(): ts.ApplicableRefactorInfo[] {
throw new Error("Not supported on the shim.");
}
getEmitOutput(fileName: string): ts.EmitOutput {
Expand Down
65 changes: 35 additions & 30 deletions src/server/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -692,48 +692,53 @@ namespace ts.server {
return response.body.map(entry => this.convertCodeActions(entry, fileName));
}

getRefactorDiagnostics(fileName: string, range: TextRange): RefactorDiagnostic[] {
const startLineOffset = this.positionToOneBasedLineOffset(fileName, range.pos);
const endLineOffset = this.positionToOneBasedLineOffset(fileName, range.end);
getRefactorDiagnostics(_fileName: string): Diagnostic[] {
return notImplemented();
}

const args: protocol.GetRefactorsForRangeRequestArgs = {
private positionOrRangeToLocationOrSpan(positionOrRange: number | TextRange, fileName: string) {
let locationOrSpan: protocol.LocationOrSpanWithPosition;
if (typeof positionOrRange === "number") {
locationOrSpan = this.positionToOneBasedLineOffset(fileName, positionOrRange);
}
else {
locationOrSpan = positionOrRange
? { start: this.positionToOneBasedLineOffset(fileName, positionOrRange.pos), end: this.positionToOneBasedLineOffset(fileName, positionOrRange.end) }
: undefined;
}
return locationOrSpan;
}

getApplicableRefactors(fileName: string, positionOrRange: number | TextRange): ApplicableRefactorInfo[] {
const args: protocol.Refactor.GetApplicableRefactorsRequestArgs = {
file: fileName,
startLine: startLineOffset.line,
startOffset: startLineOffset.offset,
endLine: endLineOffset.line,
endOffset: endLineOffset.offset,
locationOrSpan: this.positionOrRangeToLocationOrSpan(positionOrRange, fileName)
};

const request = this.processRequest<protocol.GetRefactorsForRangeRequest>(CommandNames.GetRefactorsForRange, args);
const response = this.processResponse<protocol.GetRefactorsForRangeResponse>(request);
const request = this.processRequest<protocol.Refactor.GetApplicableRefactorsRequest>(CommandNames.GetApplicableRefactors, args);
const response = this.processResponse<protocol.Refactor.GetApplicableRefactorsResponse>(request);

return response.body.map(entry => {
return <RefactorDiagnostic>{
code: entry.code,
end: this.lineOffsetToPosition(fileName, entry.end),
start: this.lineOffsetToPosition(fileName, entry.start),
text: entry.text
};
});
return response.body;
}

getCodeActionsForRefactorAtPosition(fileName: string, range: TextRange, refactorCode: number): CodeAction[] {
const startLineOffset = this.positionToOneBasedLineOffset(fileName, range.pos);
const endLineOffset = this.positionToOneBasedLineOffset(fileName, range.end);
getRefactorCodeActions(
fileName: string,
_formatOptions: FormatCodeSettings,
positionOrRange: number | TextRange,
refactorKinds?: RefactorKind[],
diagnosticCodes?: number[]) {

const args: protocol.GetCodeActionsForRefactorRequestArgs = {
const args: protocol.Refactor.GetRefactorCodeActionsRequestArgs = {
file: fileName,
startLine: startLineOffset.line,
startOffset: startLineOffset.offset,
endLine: endLineOffset.line,
endOffset: endLineOffset.offset,
refactorCode
locationOrSpan: this.positionOrRangeToLocationOrSpan(positionOrRange, fileName),
refactorKinds,
diagnosticCodes
};

const request = this.processRequest<protocol.GetCodeActionsForRefactorRequest>(CommandNames.GetCodeActionsForRefactor, args);
const response = this.processResponse<protocol.GetCodeActionsForRefactorResponse>(request);
const request = this.processRequest<protocol.Refactor.GetRefactorCodeActionsRequest>(CommandNames.GetActionsForRefactor, args);
const codeActions = this.processResponse<protocol.Refactor.GetRefactorCodeActionsResponse>(request).body;

return response.body.map(entry => this.convertCodeActions(entry, fileName));
return map(codeActions, codeAction => this.convertCodeActions(codeAction, fileName));
}

convertCodeActions(entry: protocol.CodeAction, fileName: string): CodeAction {
Expand Down
Loading

0 comments on commit 132051b

Please sign in to comment.