Skip to content

Commit

Permalink
Fixes #70: provide shortcut for applying fixes.
Browse files Browse the repository at this point in the history
  • Loading branch information
dbaeumer committed May 22, 2016
1 parent 2f33fbd commit 45d3dc8
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 58 deletions.
2 changes: 1 addition & 1 deletion eslint-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"node": "*"
},
"dependencies": {
"vscode-languageserver": "^2.0.0"
"vscode-languageserver": "^2.2.0"
},
"devDependencies": {
"typescript": "^1.8.9"
Expand Down
194 changes: 142 additions & 52 deletions eslint-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
ResponseError, RequestType, RequestHandler, NotificationType, NotificationHandler,
InitializeResult, InitializeError,
Diagnostic, DiagnosticSeverity, Position, Range, Files,
TextDocuments, TextDocument, TextDocumentSyncKind, TextEdit,
TextDocuments, TextDocument, TextDocumentSyncKind, TextEdit, TextDocumentIdentifier,
Command,
ErrorMessageTracker, IPCMessageReader, IPCMessageWriter
} from 'vscode-languageserver';
Expand Down Expand Up @@ -225,72 +225,162 @@ connection.onDidChangeWatchedFiles((params) => {
validateMany(documents.all());
});

class Fixes {
private keys: string[];

constructor (private edits: Map<AutoFix>) {
this.keys = Object.keys(edits);
}

public static overlaps(lastEdit: AutoFix, newEdit: AutoFix): boolean {
return !!lastEdit && lastEdit.edit.range[1] > newEdit.edit.range[0];
}

public isEmpty(): boolean {
return this.keys.length === 0;
}

public getDocumentVersion(): number {
return this.edits[this.keys[0]].documentVersion;
}

public getScoped(diagnostics: Diagnostic[]): AutoFix[] {
let result: AutoFix[] = [];
for(let diagnostic of diagnostics) {
let key = computeKey(diagnostic);
let editInfo = this.edits[key];
if (editInfo) {
result.push(editInfo);
}
}
return result;
}

public getAllSorted(): AutoFix[] {
let result = this.keys.map(key => this.edits[key]);
return result.sort((a, b) => {
let d = a.edit.range[0] - b.edit.range[0];
if (d !== 0) {
return d;
}
if (a.edit.range[1] === 0) {
return -1;
}
if (b.edit.range[1] === 0) {
return 1;
}
return a.edit.range[1] - b.edit.range[1];
});
}

public getOverlapFree(): AutoFix[] {
let sorted = this.getAllSorted();
if (sorted.length <= 1) {
return sorted;
}
let result: AutoFix[] = [];
let last: AutoFix = sorted[0];
result.push(last);
for (let i = 1; i < sorted.length; i++) {
let current = sorted[i];
if (!Fixes.overlaps(last, current)) {
result.push(current);
last = current;
}
}
return result;
}
}

connection.onCodeAction((params) => {
let result: Command[] = [];
let uri = params.textDocument.uri;
let textDocument = documents.get(uri);
let edits = codeActions[uri];
if (!edits) {
return result;
}

let fixes = new Fixes(edits);
if (fixes.isEmpty()) {
return result;
}

let textDocument = documents.get(uri);
let documentVersion: number = -1;
let ruleId: string;
function createTextEdit(editInfo: AutoFix): TextEdit {
return TextEdit.replace(Range.create(textDocument.positionAt(editInfo.edit.range[0]), textDocument.positionAt(editInfo.edit.range[1])), editInfo.edit.text || '');
}
if (edits) {
for(let diagnostic of params.context.diagnostics) {
let key = computeKey(diagnostic);
let editInfo = edits[key];
if (editInfo) {
documentVersion = editInfo.documentVersion;
ruleId = editInfo.ruleId;
result.push(Command.create(editInfo.label, 'eslint.applySingleFix', uri, documentVersion, [
createTextEdit(editInfo)
]));

for (let editInfo of fixes.getScoped(params.context.diagnostics)) {
documentVersion = editInfo.documentVersion;
ruleId = editInfo.ruleId;
result.push(Command.create(editInfo.label, 'eslint.applySingleFix', uri, documentVersion, [
createTextEdit(editInfo)
]));
};

if (result.length > 0) {
let same: AutoFix[] = [];
let all: AutoFix[] = [];

function getLastEdit(array: AutoFix[]): AutoFix {
let length = array.length;
if (length === 0) {
return undefined;
}
return array[length - 1];
}
if (result.length > 0) {
let same: AutoFix[] = [];
let all: AutoFix[] = [];
let fixes: AutoFix[] = Object.keys(edits).map(key => edits[key]);
fixes = fixes.sort((a, b) => {
let d = a.edit.range[0] - b.edit.range[0];
if (d !== 0) {
return d;
}
if (a.edit.range[1] === 0) {
return -1;
}
if (b.edit.range[1] === 0) {
return 1;
}
return a.edit.range[1] - b.edit.range[1];
});
function overlaps(lastEdit: AutoFix, newEdit: AutoFix): boolean {
return !!lastEdit && lastEdit.edit.range[1] > newEdit.edit.range[0];

for (let editInfo of fixes.getAllSorted()) {
if (documentVersion === -1) {
documentVersion = editInfo.documentVersion;
}
function getLastEdit(array: AutoFix[]): AutoFix {
let length = array.length;
if (length === 0) {
return undefined;
}
return array[length - 1];
if (editInfo.ruleId === ruleId && !Fixes.overlaps(getLastEdit(same), editInfo)) {
same.push(editInfo);
}
for (let editInfo of fixes) {
if (documentVersion === -1) {
documentVersion = editInfo.documentVersion;
}
if (editInfo.ruleId === ruleId && !overlaps(getLastEdit(same), editInfo)) {
same.push(editInfo);
}
if (!overlaps(getLastEdit(all), editInfo)) {
all.push(editInfo);
}
}
if (same.length > 1) {
result.push(Command.create(`Fix all ${ruleId} problems`, 'eslint.applySameFixes', uri, documentVersion, same.map(createTextEdit)));
if (!Fixes.overlaps(getLastEdit(all), editInfo)) {
all.push(editInfo);
}
if (all.length > 1) {
result.push(Command.create(`Fix all auto-fixable problems`, 'eslint.applyAllFixes', uri, documentVersion, all.map(createTextEdit)));
}
if (same.length > 1) {
result.push(Command.create(`Fix all ${ruleId} problems`, 'eslint.applySameFixes', uri, documentVersion, same.map(createTextEdit)));
}
if (all.length > 1) {
result.push(Command.create(`Fix all auto-fixable problems`, 'eslint.applyAllFixes', uri, documentVersion, all.map(createTextEdit)));
}
}
return result;
});

interface AllFixesParams {
textDocument: TextDocumentIdentifier;
}

interface AllFixesResult {
documentVersion: number,
edits: TextEdit[]
}

namespace AllFixesRequest {
export const type: RequestType<AllFixesParams, AllFixesResult, void> = { get method() { return 'textDocument/eslint/allFixes'; } };
}

connection.onRequest(AllFixesRequest.type, (params) => {
let result: AllFixesResult = null;
let uri = params.textDocument.uri;
let textDocument = documents.get(uri);
let edits = codeActions[uri];
function createTextEdit(editInfo: AutoFix): TextEdit {
return TextEdit.replace(Range.create(textDocument.positionAt(editInfo.edit.range[0]), textDocument.positionAt(editInfo.edit.range[1])), editInfo.edit.text || '');
}

if (edits) {
let fixes = new Fixes(edits);
if (!fixes.isEmpty()) {
result = {
documentVersion: fixes.getDocumentVersion(),
edits: fixes.getOverlapFree().map(createTextEdit)
}
}
}
Expand Down
36 changes: 33 additions & 3 deletions eslint/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,22 @@
'use strict';

import * as path from 'path';
import { workspace, window, commands, Disposable, ExtensionContext } from 'vscode';
import { LanguageClient, LanguageClientOptions, SettingMonitor, RequestType, TransportKind, TextEdit, Protocol2Code } from 'vscode-languageclient';
import { workspace, window, commands, Disposable, ExtensionContext, Command } from 'vscode';
import { LanguageClient, LanguageClientOptions, SettingMonitor, RequestType, TransportKind, TextDocumentIdentifier, TextEdit, Protocol2Code } from 'vscode-languageclient';


interface AllFixesParams {
textDocument: TextDocumentIdentifier;
}

interface AllFixesResult {
documentVersion: number,
edits: TextEdit[]
}

namespace AllFixesRequest {
export const type: RequestType<AllFixesParams, AllFixesResult, void> = { get method() { return 'textDocument/eslint/allFixes'; } };
}

export function activate(context: ExtensionContext) {
// We need to go one level up since an extension compile the js code into
Expand Down Expand Up @@ -46,10 +60,26 @@ export function activate(context: ExtensionContext) {
}
}

function fixAllProblems() {
let textEditor = window.activeTextEditor;
if (!textEditor) {
return;
}
let uri: string = textEditor.document.uri.toString();
client.sendRequest(AllFixesRequest.type, { textDocument: { uri }}).then((result) => {
if (result) {
applyTextEdits(uri, result.documentVersion, result.edits);
}
}, (error) => {
window.showErrorMessage('Failed to apply ESLint fixes to the document. Please consider opening an issue with steps to reproduce.');
});
}

context.subscriptions.push(
new SettingMonitor(client, 'eslint.enable').start(),
commands.registerCommand('eslint.applySingleFix', applyTextEdits),
commands.registerCommand('eslint.applySameFixes', applyTextEdits),
commands.registerCommand('eslint.applyAllFixes', applyTextEdits)
commands.registerCommand('eslint.applyAllFixes', applyTextEdits),
commands.registerCommand('eslint.fixAllProblems', fixAllProblems)
);
}
11 changes: 9 additions & 2 deletions eslint/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "vscode-eslint",
"displayName": "ESLint",
"description": "Integrates ESLint into VS Code.",
"version": "0.10.15",
"version": "0.10.16",
"author": "Microsoft Corporation",
"license": "MIT",
"repository": {
Expand Down Expand Up @@ -43,6 +43,13 @@
}
}
},
"commands": [
{
"title": "Fix all auto-fixable problems",
"category": "ESLint",
"command": "eslint.fixAllProblems"
}
],
"jsonValidation": [
{
"fileMatch": ".eslintrc",
Expand All @@ -66,6 +73,6 @@
"typescript": "^1.8.9"
},
"dependencies": {
"vscode-languageclient": "^2.0.0"
"vscode-languageclient": "^2.2.0"
}
}

0 comments on commit 45d3dc8

Please sign in to comment.