Skip to content
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

Add infrastructure for refactors #14624

Closed
wants to merge 16 commits into from

Conversation

zhengbli
Copy link
Contributor

@zhengbli zhengbli commented Mar 13, 2017

This PR adds the infrastructure for refactors.

The PR adds one new kind of code fixes as well as refactors:

  1. "diagnostic-generating code fixes": a special kind of code fixes that would generate non-error diagnostics. After normal syntactic check and semantic check, the language service will do an extra pass of the AST to get the non-error diagnostics generated by registered "diagnostic-generating code fixes". Then when the user interacts with the new diagnostics in code, a query for corresponding code actions will be triggered, just like with other code fixes.

  2. "refactors": to get a refactor it is a two-step procedure. First, the editor will ask about all "applicable refactor info" at a given location / range, the returned "applicable refactor info" would only contain basic information like refactor names, it wouldn't compute the code actions yet. Then after the user chose a particular refactor, then the editor will send another request to compute the corresponding code actions.

host.runQueuedImmediateCallbacks();
assert.equal(host.getOutput().length, 2, "expect 2 messages");
assert.equal(host.getOutput().length, 1, "expect 1 messages");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"messages" should be "message"

@@ -692,6 +692,50 @@ 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this crash if range === undefined ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here the range is not supposed to be nullable, as we only use it to test the session API GetRefactorsForRange. Will update

* Instances of this interface specify errorcodes on a specific location in a sourcefile.
*/
export interface CodeFixRequestArgs extends FileRequestArgs {
export interface TextRangeRequestArgs extends FileRequestArgs {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you name the first two members line and offset to more closely coincide with FileLocationRequestArgs andLocation? Or use a Location for the start and end?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe FileRangeRequestArgs would fit better. But using location for start and end would be an unnecessary breaking change for editors already consuming this type.

return { startPosition, endPosition };

function getStartPosition() {
return args.startPosition !== undefined ? args.startPosition : scriptInfo.lineOffsetToPosition(args.startLine, args.startOffset);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to save the result in args.startPosition (resp. args.endPosition)?

/* @internal */
namespace ts {
interface BaseRefactor {
/** Description of the refactor to display in the UI of the editor */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this have a property canBeSuggested: boolean; ?

export type Refactor = SuggestableRefactor | NonSuggestableRefactor;

export interface LightRefactorContext {
nonBoundSourceFile: SourceFile;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a comment explaining what bound and nonBound means?

return results;
}

export function getSuggestedRefactorDiagnosticsForNode(node: Node, context: RefactorContext) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be right to name this getSuggestable...? That would be more consistent with the naming of the interfaces.

}
}

function getCodeActionsForRefactorAtPosition(
Copy link
Contributor

@aozgaa aozgaa Mar 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a convention for when we split function parameters across multiple lines?

Copy link
Contributor Author

@zhengbli zhengbli Mar 20, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so, though normally i do that when I have to use the horizontal scroll bar on my laptop if putting all in one line.

}

export interface GetCodeActionsForRefactorResponse extends Response {
body?: CodeAction[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do not use an array. make it an map.

position?: number;
}

export namespace Refactor {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we do not use namespaces anywhere else.

}

export interface ApplicableRefactorInfo {
refactorKind: number;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would call this name and make it string.

}

export interface GetApplicableRefactorsResponse extends Response {
body?: ApplicableRefactorInfo[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would make this an object instead of an array.


export interface GetRefactorCodeActionsRequestArgs extends FileLocationOrSpanWithPositionRequestArgs {
/* The kind of the applicable refactor */
refactorKinds?: number[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probally just one refactoring to ask for code actions for

/* The kind of the applicable refactor */
refactorKinds?: number[];
/* The diagnostic code of a refactor diagnostic */
diagnosticCodes?: number[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do not think we need this, it should be covered by getCodeActions

const { startPosition, endPosition } = this.getStartAndEndPosition(args, scriptInfo);
textRange = { pos: startPosition, end: endPosition };
}
return { position, textRange };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

position may be undefined, is this intended? Initialize at line 1450 if so

return args.endPosition !== undefined ? args.endPosition : scriptInfo.lineOffsetToPosition(args.endLine, args.endOffset);
}
private getStartAndEndPosition(args: protocol.FileRangeRequestArgs, scriptInfo: ScriptInfo) {
const startPosition = args.startPosition !== undefined
Copy link
Member

@RyanCavanaugh RyanCavanaugh Apr 18, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really confusing, can we just use a normal if here

export function getCodeFixDiagnosticsForNode(context: CodeFixDiagnoseContext, node: Node): Diagnostic[] | undefined {
let result: Diagnostic[];
for (const codeFix of diagnosticGeneratingCodeFixes) {
const newDiag = codeFix.createCodeFixDiagnosticIfApplicable(node, context);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This member is declared as optional, but isn't checked for undefined in this method

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in line 41 it already checked the existence of createCodeFixDiagnosticIfApplicable

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

isApplicableForPositionOrRange(context: LightRefactorContext, positionOrRange: number | TextRange): boolean;
}

export interface LightRefactorContext {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment defining what "Light" means

return results;
}

export function getRefactorCodeActions(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this return an empty array vs getApplicableRefactors returns undefined if the array would have been empty?

@RyanCavanaugh
Copy link
Member

Should we add a trivial refactor with this just so we can have some testcases?

@zhengbli
Copy link
Contributor Author

Sure, I'm adding one now will update soon

@zhengbli
Copy link
Contributor Author

I added two things:

  1. a diagnostic-generating code fix: converting functions that has prototype assignment to ES6 classes
  2. a sample refactor: add Async suffix to async function declarations

@RyanCavanaugh
Copy link
Member

@mhegazy any other comments?

@mhegazy
Copy link
Contributor

mhegazy commented May 19, 2017

closing in favor of #15569

@mhegazy mhegazy closed this May 19, 2017
@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants