Skip to content

Commit

Permalink
VSCode Extension: Add diagnostics and code actions (belav#1376)
Browse files Browse the repository at this point in the history
Fixes belav#1375
Fixes belav#659

A lot of this code has been taken from [one of my open source
extensions](https://github.com/badsyntax/vscode-spotless-gradle).

This approach uses the
[prettier-linter-helpers](https://www.npmjs.com/package/prettier-linter-helpers)
package to generate diffs of strings (unformatted code against formatted
code) which is used to provide diagnostic information and code actions.
This allows for formatting parts of code.

<img width="1479" alt="Screenshot 2024-11-07 at 21 55 03"
src="https://github.com/user-attachments/assets/93e643fc-91c6-4684-882f-3445128b7580">
<img width="547" alt="Screenshot 2024-11-07 at 22 26 38"
src="https://github.com/user-attachments/assets/905cb8f6-87d7-499b-83ca-d470cc6e9d44">

---------

Co-authored-by: Richard Willis <[email protected]>
Co-authored-by: Lasath Fernando <[email protected]>
  • Loading branch information
3 people authored and pisolofin committed Dec 14, 2024
1 parent bc0c4f5 commit 1a1416c
Show file tree
Hide file tree
Showing 9 changed files with 527 additions and 84 deletions.
13 changes: 0 additions & 13 deletions Src/CSharpier.VSCode/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,3 @@ dotnet tool install csharpier

# rebuild container image
```

## Limitations

Format Selection is not supported.

Only `"editor.formatOnSaveMode" : "file"` is supported. If using other modes, you can set `file` by scoping the setting:
```json
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "modifications"
"[csharp]": {
"editor.formatOnSaveMode": "file"
}
```
49 changes: 46 additions & 3 deletions Src/CSharpier.VSCode/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Src/CSharpier.VSCode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"@types/mocha": "9.0.0",
"@types/node": "14.x",
"@types/node-fetch": "^2.6.11",
"@types/prettier-linter-helpers": "^1.0.4",
"@types/semver": "7.3.9",
"@types/vscode": "1.60.0",
"prettier": "2.4.1",
Expand All @@ -88,6 +89,7 @@
"xml-js": "1.6.11"
},
"dependencies": {
"node-fetch": "^2.7.0"
"node-fetch": "^2.7.0",
"prettier-linter-helpers": "^1.0.0"
}
}
229 changes: 229 additions & 0 deletions Src/CSharpier.VSCode/src/DiagnosticsService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import * as vscode from "vscode";
import { Difference, generateDifferences, showInvisibles } from "prettier-linter-helpers";
import { FixAllCodeActionsCommand } from "./FixAllCodeActionCommand";
import { Logger } from "./Logger";
import { FormatDocumentProvider } from "./FormatDocumentProvider";

const DIAGNOSTICS_ID = "csharpier";
const DIAGNOSTICS_SOURCE_ID = "diagnostic";

export interface CsharpierDiff {
source: string;
formattedSource: string;
differences: Difference[];
}

export class DiagnosticsService implements vscode.CodeActionProvider, vscode.Disposable {
public static readonly quickFixCodeActionKind =
vscode.CodeActionKind.QuickFix.append(DIAGNOSTICS_ID);
public static metadata: vscode.CodeActionProviderMetadata = {
providedCodeActionKinds: [DiagnosticsService.quickFixCodeActionKind],
};

private readonly diagnosticCollection: vscode.DiagnosticCollection;
private readonly diagnosticDifferenceMap: Map<vscode.Diagnostic, Difference> = new Map();
private readonly codeActionsProvider: vscode.Disposable;
private readonly disposables: vscode.Disposable[] = [];

constructor(
private readonly formatDocumentProvider: FormatDocumentProvider,
private readonly documentSelector: Array<vscode.DocumentFilter>,
private readonly logger: Logger,
) {
this.diagnosticCollection = vscode.languages.createDiagnosticCollection(DIAGNOSTICS_ID);
this.codeActionsProvider = vscode.languages.registerCodeActionsProvider(
this.documentSelector,
this,
DiagnosticsService.metadata,
);
this.registerEditorEvents();
}

public dispose(): void {
for (const disposable of this.disposables) {
disposable.dispose();
}
this.diagnosticCollection.dispose();
this.codeActionsProvider.dispose();
}

private handleChangeTextDocument(document: vscode.TextDocument): void {
void this.runDiagnostics(document);
}

public async runDiagnostics(document: vscode.TextDocument): Promise<void> {
const shouldRunDiagnostics =
this.documentSelector.some(selector => selector.language === document.languageId) &&
!!vscode.workspace.getWorkspaceFolder(document.uri);
if (shouldRunDiagnostics) {
try {
const diff = await this.getDiff(document);
this.updateDiagnostics(document, diff);
} catch (e) {
this.logger.error(`Unable to provide diagnostics: ${(e as Error).message}`);
}
}
}

public updateDiagnostics(document: vscode.TextDocument, diff: CsharpierDiff): void {
const diagnostics = this.getDiagnostics(document, diff);
this.diagnosticCollection.set(document.uri, diagnostics);
}

private registerEditorEvents(): void {
const activeDocument = vscode.window.activeTextEditor?.document;
if (activeDocument) {
void this.runDiagnostics(activeDocument);
}

const onDidChangeTextDocument = vscode.workspace.onDidChangeTextDocument(
(e: vscode.TextDocumentChangeEvent) => {
if (
e.contentChanges.length &&
vscode.window.activeTextEditor?.document === e.document
) {
this.handleChangeTextDocument(e.document);
}
},
);

const onDidChangeActiveTextEditor = vscode.window.onDidChangeActiveTextEditor(
(editor?: vscode.TextEditor) => {
if (editor) {
void this.runDiagnostics(editor.document);
}
},
);

this.disposables.push(
onDidChangeTextDocument,
onDidChangeActiveTextEditor,
this.diagnosticCollection,
);
}

private getDiagnostics(
document: vscode.TextDocument,
diff: CsharpierDiff,
): vscode.Diagnostic[] {
const diagnostics: vscode.Diagnostic[] = [];
for (const difference of diff.differences) {
const diagnostic = this.getDiagnostic(document, difference);
this.diagnosticDifferenceMap.set(diagnostic, difference);
diagnostics.push(diagnostic);
}
return diagnostics;
}

private getDiagnostic(
document: vscode.TextDocument,
difference: Difference,
): vscode.Diagnostic {
const range = this.getRange(document, difference);
const message = this.getMessage(difference);
const diagnostic = new vscode.Diagnostic(range, message);
diagnostic.source = DIAGNOSTICS_ID;
diagnostic.code = DIAGNOSTICS_SOURCE_ID;
return diagnostic;
}

private getMessage(difference: Difference): string {
switch (difference.operation) {
case generateDifferences.INSERT:
return `Insert ${showInvisibles(difference.insertText!)}`;
case generateDifferences.REPLACE:
return `Replace ${showInvisibles(difference.deleteText!)} with ${showInvisibles(
difference.insertText!,
)}`;
case generateDifferences.DELETE:
return `Delete ${showInvisibles(difference.deleteText!)}`;
default:
return "";
}
}

private getRange(document: vscode.TextDocument, difference: Difference): vscode.Range {
if (difference.operation === generateDifferences.INSERT) {
const start = document.positionAt(difference.offset);
return new vscode.Range(start.line, start.character, start.line, start.character);
}
const start = document.positionAt(difference.offset);
const end = document.positionAt(difference.offset + difference.deleteText!.length);
return new vscode.Range(start.line, start.character, end.line, end.character);
}

private async getDiff(document: vscode.TextDocument): Promise<CsharpierDiff> {
const source = document.getText();
const formattedSource =
(await this.formatDocumentProvider.formatDocument(document)) ?? source;
const differences = generateDifferences(source, formattedSource);
return {
source,
formattedSource,
differences,
};
}

public provideCodeActions(
document: vscode.TextDocument,
range: vscode.Range | vscode.Selection,
): vscode.CodeAction[] {
let totalDiagnostics = 0;
const codeActions: vscode.CodeAction[] = [];
this.diagnosticCollection.forEach(
(uri: vscode.Uri, diagnostics: readonly vscode.Diagnostic[]) => {
if (document.uri.fsPath !== uri.fsPath) {
return;
}
diagnostics.forEach((diagnostic: vscode.Diagnostic) => {
totalDiagnostics += 1;
if (!range.isEqual(diagnostic.range)) {
return;
}
const difference = this.diagnosticDifferenceMap.get(diagnostic);
codeActions.push(
this.getQuickFixCodeAction(document.uri, diagnostic, difference!),
);
});
},
);
if (totalDiagnostics > 1) {
codeActions.push(this.getQuickFixAllProblemsCodeAction(document, totalDiagnostics));
}
return codeActions;
}

private getQuickFixCodeAction(
uri: vscode.Uri,
diagnostic: vscode.Diagnostic,
difference: Difference,
): vscode.CodeAction {
const action = new vscode.CodeAction(
`Fix this ${DIAGNOSTICS_ID} problem`,
DiagnosticsService.quickFixCodeActionKind,
);
action.edit = new vscode.WorkspaceEdit();
if (difference.operation === generateDifferences.INSERT) {
action.edit.insert(uri, diagnostic.range.start, difference.insertText!);
} else if (difference.operation === generateDifferences.REPLACE) {
action.edit.replace(uri, diagnostic.range, difference.insertText!);
} else if (difference.operation === generateDifferences.DELETE) {
action.edit.delete(uri, diagnostic.range);
}
return action;
}

private getQuickFixAllProblemsCodeAction(
document: vscode.TextDocument,
totalDiagnostics: number,
): vscode.CodeAction {
const title = `Fix all ${DIAGNOSTICS_ID} problems (${totalDiagnostics})`;
const action = new vscode.CodeAction(title, DiagnosticsService.quickFixCodeActionKind);
action.command = {
title,
command: FixAllCodeActionsCommand.Id,
arguments: [document],
};
return action;
}
}
Loading

0 comments on commit 1a1416c

Please sign in to comment.