Skip to content

Commit

Permalink
Initial quick fix implementation
Browse files Browse the repository at this point in the history
Some basic quick fixes has been implemted along with a special
request handler.
- Implement interface; for methods and properties
- Implement abstract class; for methods and properties
- Add import statement (ES6 / AMD)
- Add class method / member
- Type cast to "any"

Related microsoft#6943
  • Loading branch information
kisstkondoros committed Jun 24, 2016
1 parent 97d7aa5 commit 0c414a5
Show file tree
Hide file tree
Showing 14 changed files with 853 additions and 0 deletions.
29 changes: 29 additions & 0 deletions src/server/protocol.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,20 @@ declare namespace ts.server.protocol {
file: string;
}

export interface SimpleTextSpan {
start: number;
length: number;
}

export class TextChange {
span: SimpleTextSpan;
newText: string;
}

export interface Refactoring extends TextChange {
filePath: string;
}

/**
* Definition response message. Gives text range for definition.
*/
Expand Down Expand Up @@ -343,6 +357,21 @@ declare namespace ts.server.protocol {
findInStrings?: boolean;
}

export interface QuickFixRequest extends FileLocationRequest {
}

export interface QuickFixResponse extends Response {
body?: QuickFixResponseBody;
}

export interface QuickFix {
display: string;
refactorings: Refactoring[];
}

export interface QuickFixResponseBody {
quickFixes?: QuickFix[];
}

/**
* Rename request; value of command field is "rename". Return
Expand Down
44 changes: 44 additions & 0 deletions src/server/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
/// <reference path="..\services\services.ts" />
/// <reference path="protocol.d.ts" />
/// <reference path="editorServices.ts" />
/// <reference path="..\services\quickfixes\quickFixRegistry.ts" />

namespace ts.server {
const spaceCache: string[] = [];
Expand Down Expand Up @@ -125,6 +126,7 @@ namespace ts.server {
export const TypeDefinition = "typeDefinition";
export const ProjectInfo = "projectInfo";
export const ReloadProjects = "reloadProjects";
export const QuickFixes = "quickfix";
export const Unknown = "unknown";
}

Expand Down Expand Up @@ -435,6 +437,44 @@ namespace ts.server {
return projectInfo;
}

private getQuickFixes(fileName: string, line: number, offset: number): protocol.QuickFixResponseBody {
fileName = ts.normalizePath(fileName);

const possibleFixes: protocol.QuickFix[] = [];
const project = this.projectService.getProjectForFile(fileName);
const syntacticDiags = project.compilerService.languageService.getSyntacticDiagnostics(fileName);
const semanticDiags = project.compilerService.languageService.getSemanticDiagnostics(fileName);
const sourceFile = project.compilerService.languageService.getProgram().getSourceFile(fileName);

const positionNode = getTokenAtPosition(sourceFile, project.compilerService.host.lineOffsetToPosition(fileName, line, offset));
const diagnostics: Diagnostic[] = syntacticDiags.concat(semanticDiags).filter(diag => {
const isSameNode = diag.start == positionNode.getStart();
return isSameNode;
});

const param: ts.quickFix.QuickFixQueryInformation = {
project: project,
service: project.compilerService.languageService,
program: project.program,
typeChecker: project.program.getTypeChecker(),
sourceFile: sourceFile,
positionErrors: diagnostics
};

quickFixRegistry.QuickFixes.allQuickFixes.forEach(possibleQuickfix => {
try {
possibleQuickfix.provideFix(param).forEach(fix => {
if (fix && fix.display && fix.refactorings && fix.refactorings.length > 0) {
possibleFixes.push({ display: fix.display, refactorings: fix.refactorings });
}
});
}
catch (error) {
}
});
return { quickFixes: possibleFixes };
}

private getRenameLocations(line: number, offset: number, fileName: string, findInComments: boolean, findInStrings: boolean): protocol.RenameResponseBody {
const file = ts.normalizePath(fileName);
const info = this.projectService.getScriptInfo(file);
Expand Down Expand Up @@ -1043,6 +1083,10 @@ namespace ts.server {
const renameArgs = <protocol.RenameRequestArgs>request.arguments;
return { response: this.getRenameLocations(renameArgs.line, renameArgs.offset, renameArgs.file, renameArgs.findInComments, renameArgs.findInStrings), responseRequired: true };
},
[CommandNames.QuickFixes]: (request: protocol.Request) => {
const quickFixArgs = <protocol.FileLocationRequestArgs>request.arguments;
return { response: this.getQuickFixes(quickFixArgs.file, quickFixArgs.line, quickFixArgs.offset), responseRequired: true };
},
[CommandNames.Open]: (request: protocol.Request) => {
const openArgs = <protocol.OpenRequestArgs>request.arguments;
let scriptKind: ScriptKind;
Expand Down
22 changes: 22 additions & 0 deletions src/services/quickfixes/quickFix.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) Microsoft. All rights reserved. Licensed under the Apache License, Version 2.0.
// See LICENSE.txt in the project root for complete license information.

/// <reference path='../services.ts' />
/// <reference path='../../server/session.ts' />

namespace ts.quickFix {

export interface QuickFixQueryInformation {
project: server.Project;
service: LanguageService;
program: Program;
typeChecker: TypeChecker;
sourceFile: SourceFile;
positionErrors: Diagnostic[];
}

export interface QuickFix {
provideFix(info: QuickFixQueryInformation): ts.server.protocol.QuickFix[];
}

}
25 changes: 25 additions & 0 deletions src/services/quickfixes/quickFixRegistry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) Microsoft. All rights reserved. Licensed under the Apache License, Version 2.0.
// See LICENSE.txt in the project root for complete license information.

/// <reference path=".\quickFix.ts" />
/// <reference path=".\semantic\addClassMember.ts" />
/// <reference path=".\semantic\addClassMethod.ts" />
/// <reference path=".\semantic\addImportFromStatement.ts" />
/// <reference path=".\semantic\addImportStatement.ts" />
/// <reference path=".\semantic\typeAssertPropertyAccessToAny.ts" />
/// <reference path=".\semantic\implementInterface.ts" />
/// <reference path=".\semantic\implementAbstractClass.ts" />

namespace ts.quickFixRegistry {
export class QuickFixes{
public static allQuickFixes: quickFix.QuickFix[] = [
new quickFix.semantic.AddClassMethod(),
new quickFix.semantic.AddClassMember(),
new quickFix.semantic.AddImportFromStatement(),
new quickFix.semantic.AddImportStatement(),
new quickFix.semantic.TypeAssertPropertyAccessToAny(),
new quickFix.semantic.ImplementInterface(),
new quickFix.semantic.ImplementAbstractClass()
];
}
}
99 changes: 99 additions & 0 deletions src/services/quickfixes/semantic/addClassMember.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright (c) Microsoft. All rights reserved. Licensed under the Apache License, Version 2.0.
// See LICENSE.txt in the project root for complete license information.

/// <reference path='./messageAndCodeBasedQuickFixBase.ts' />
namespace ts.quickFix.semantic {
export class AddClassMember extends MessageAndCodeBasedQuickFixBase {
protected getPattern(): RegExp {
return /Property \'(\w+)\' does not exist on type \'(\w+)\'./;
}
protected getSupportedErrorCode(): number {
return Diagnostics.Property_0_does_not_exist_on_type_1.code;
}
protected getIdentifierAndClassNameFromMatch(info: QuickFixQueryInformation, error: Diagnostic, match: RegExpMatchArray): IdentifierAndClassName[] {
return [{ identifierName: match[1], className: match[2] }];
}
protected getDisplay(context: QuickFixContext): string {
var {identifierName, className} = context.identifierAndClassName;
return `Add ${identifierName} to ${className}`
}

private getLastNameAfterDot(text: string) {
return text.substr(text.lastIndexOf('.') + 1);
}

private getTypeStringForNode(node: Node, typeChecker: TypeChecker) {
var type = typeChecker.getTypeAtLocation(node);
return displayPartsToString(ts.typeToDisplayParts(typeChecker, type)).replace(/\s+/g, ' ');
}
protected getRefactorings(context: QuickFixContext) {
var {identifierName, className} = context.identifierAndClassName;
var {info} = context;

// Get the type of the stuff on the right if its an assignment
var typeString = 'any';
var parentOfParent = context.identifier.parent.parent;
if (parentOfParent.kind === SyntaxKind.BinaryExpression
&& (<BinaryExpression>parentOfParent).operatorToken.getText().trim() === '=') {

let binaryExpression = <BinaryExpression>parentOfParent;
typeString = this.getTypeStringForNode(binaryExpression.right, info.typeChecker);
}
else if (parentOfParent.kind === SyntaxKind.CallExpression) {
let callExp = <CallExpression>parentOfParent;
let typeStringParts = ['('];

// Find the number of arguments
let args: string[] = [];
callExp.arguments.forEach(arg => {
var argName = (this.getLastNameAfterDot(arg.getText()));
var argType = this.getTypeStringForNode(arg, info.typeChecker);

args.push(`${argName}: ${argType}`);
});
typeStringParts.push(args.join(', '));

// TODO: infer the return type as well if the next parent is an assignment
// Currently its `any`
typeStringParts.push(') => any');
typeString = typeStringParts.join('');
}

// Find the containing class declaration
var memberTarget = getNodeByKindAndName([info.sourceFile], SyntaxKind.ClassDeclaration, className) ||
getNodeByKindAndName(info.program.getSourceFiles(), SyntaxKind.ClassDeclaration, className);
if (!memberTarget) {
// Find the containing interface declaration
memberTarget = getNodeByKindAndName([info.sourceFile], SyntaxKind.InterfaceDeclaration, className) ||
getNodeByKindAndName(info.program.getSourceFiles(), SyntaxKind.InterfaceDeclaration, className);
}
if (!memberTarget) {
return [];
}

// The following code will be same (and typesafe) for either class or interface
let targetDeclaration = <ClassDeclaration | InterfaceDeclaration>memberTarget;

// Then the first brace
let firstBrace = targetDeclaration.getChildren().filter(x => x.kind === SyntaxKind.OpenBraceToken)[0];

let formatCodeOptions = info.project.projectService.getFormatCodeOptions();
var newLine = formatCodeOptions.NewLineCharacter;
// And the correct indent
var indentLength = info.service.getIndentationAtPosition(memberTarget.getSourceFile().fileName, firstBrace.end, formatCodeOptions);
var indent = formatting.getIndentationString(indentLength + formatCodeOptions.IndentSize, formatCodeOptions);

// And add stuff after the first brace
let refactoring: server.protocol.Refactoring = {
span: {
start: firstBrace.end,
length: 0
},
newText: `${newLine}${indent}${identifierName}: ${typeString};`,
filePath: targetDeclaration.getSourceFile().fileName
};

return [refactoring];
}
}
}
Loading

0 comments on commit 0c414a5

Please sign in to comment.