diff --git a/src/server/protocol.d.ts b/src/server/protocol.d.ts index f0dfe4eb130..e8919f843eb 100644 --- a/src/server/protocol.d.ts +++ b/src/server/protocol.d.ts @@ -203,6 +203,53 @@ declare namespace ts.server.protocol { position?: number; } + /** + * Request for the available codefixed at a specific position. + */ + export interface CodeFixRequest extends Request { + arguments: CodeFixRequestArgs; + } + + /** + * Instances of this interface specify errorcodes on a specific location in a sourcefile. + */ + export interface CodeFixRequestArgs extends FileRequestArgs { + /** + * The line number for the request (1-based). + */ + startLine?: number; + + /** + * The character offset (on the line) for the request (1-based). + */ + startOffset?: number; + + /** + * Position (can be specified instead of line/offset pair) + */ + startPosition?: number; + + /** + * The line number for the request (1-based). + */ + endLine?: number; + + /** + * The character offset (on the line) for the request (1-based). + */ + endOffset?: number; + + /** + * Position (can be specified instead of line/offset pair) + */ + endPosition?: number; + + /** + * Errorcodes we want to get the fixes for. + */ + errorCodes?: string[] + } + /** * A request whose arguments specify a file location (file, line, col). */ @@ -428,7 +475,6 @@ declare namespace ts.server.protocol { findInStrings?: boolean; } - /** * Rename request; value of command field is "rename". Return * response giving the file locations that reference the symbol @@ -873,6 +919,18 @@ declare namespace ts.server.protocol { newText: string; } + export interface FileCodeEdits { + fileName: string; + textChanges: CodeEdit[]; + } + + export interface CodeActionResponse extends Response { + /** Description of the code action to display in the UI of the editor */ + description: string; + /** Text changes to apply to each file as part of the code action */ + changes: FileCodeEdits[]; + } + /** * Format and format on key response message. */ @@ -1518,4 +1576,40 @@ declare namespace ts.server.protocol { export interface NavBarResponse extends Response { body?: NavigationBarItem[]; } + + export interface CodeAction { + /** + * Description of the code action to display in the UI of the editor. + */ + description: string; + + /** + * Changes to apply to each file as part of the code action. + */ + changes: FileTextChanges[] + } + + export interface FileTextChanges { + /** + * File to apply the change to. + */ + fileName: string; + + /** + * Changes to apply to the file. + */ + textChanges: TextChange[]; + } + + export class TextChange { + /** + * The span for the text change. + */ + span: TextSpan; + + /** + * New text for the span, can be an empty string if we want to delete text. + */ + newText: string; + } } diff --git a/src/server/session.ts b/src/server/session.ts index d076d5deb6d..ff4384aafbd 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -134,6 +134,8 @@ namespace ts.server { export const NameOrDottedNameSpan = "nameOrDottedNameSpan"; export const BreakpointStatement = "breakpointStatement"; export const CompilerOptionsForInferredProjects = "compilerOptionsForInferredProjects"; + export const GetCodeFixes = "getCodeFixes"; + export const GetCodeFixesFull = "getCodeFixes-full"; } export function formatMessage(msg: T, logger: server.Logger, byteLength: (s: string, encoding: string) => number, newLine: string): string { @@ -751,7 +753,7 @@ namespace ts.server { return this.getFileAndProjectWorker(args.file, args.projectFileName, /*refreshInferredProjects*/ false, errorOnMissingProject); } - private getFileAndProjectWorker(uncheckedFileName: string, projectFileName: string, refreshInferredProjects: boolean, errorOnMissingProject: boolean) { + private getFileAndProjectWorker(uncheckedFileName: string, projectFileName: string, refreshInferredProjects: boolean, errorOnMissingProject: boolean) { const file = toNormalizedPath(uncheckedFileName); const project: Project = this.getProject(projectFileName) || this.projectService.getDefaultProjectForFile(file, refreshInferredProjects); if (!project && errorOnMissingProject) { @@ -1199,6 +1201,48 @@ namespace ts.server { } } + private getCodeFixes(args: protocol.CodeFixRequestArgs, simplifiedResult: boolean): protocol.CodeAction[] | CodeAction[] { + const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); + + const scriptInfo = project.getScriptInfoForNormalizedPath(file); + const startPosition = getStartPosition(); + const endPosition = getEndPosition(); + + const codeActions = project.getLanguageService().getCodeFixesAtPosition(file, startPosition, endPosition, args.errorCodes); + if (!codeActions) { + return undefined; + } + if (simplifiedResult) { + return codeActions.map(mapCodeAction); + } else { + return codeActions; + } + + function mapCodeAction(source: CodeAction): protocol.CodeAction { + return { + description: source.description, + changes: source.changes.map(change => ({ + fileName: change.fileName, + textChanges: change.textChanges.map(textChange => ({ + span: { + start: scriptInfo.positionToLineOffset(textChange.span.start), + end: scriptInfo.positionToLineOffset(textChange.span.start + textChange.span.length) + }, + newText: textChange.newText + })) + })) + }; + } + + 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 getBraceMatching(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.TextSpan[] | TextSpan[] { const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); @@ -1521,6 +1565,12 @@ namespace ts.server { [CommandNames.ReloadProjects]: (request: protocol.ReloadProjectsRequest) => { this.projectService.reloadProjects(); return this.notRequired(); + }, + [CommandNames.GetCodeFixes]: (request: protocol.CodeFixRequest) => { + return this.requiredResponse(this.getCodeFixes(request.arguments, /*simplifiedResult*/ true)); + }, + [CommandNames.GetCodeFixesFull]: (request: protocol.CodeFixRequest) => { + return this.requiredResponse(this.getCodeFixes(request.arguments, /*simplifiedResult*/ false)); } }); diff --git a/src/server/utilities.ts b/src/server/utilities.ts index 3f99da9977e..beceab79229 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -201,7 +201,8 @@ namespace ts.server { dispose: (): any => throwLanguageServiceIsDisabledError(), getCompletionEntrySymbol: (): any => throwLanguageServiceIsDisabledError(), getImplementationAtPosition: (): any => throwLanguageServiceIsDisabledError(), - getSourceFile: (): any => throwLanguageServiceIsDisabledError() + getSourceFile: (): any => throwLanguageServiceIsDisabledError(), + getCodeFixesAtPosition: (): any => throwLanguageServiceIsDisabledError() }; export interface ServerLanguageServiceHost {