Skip to content

Commit 84b775f

Browse files
badsyntaxrichard-willis-chevinshocklateboy92
authored
VSCode Extension: Add diagnostics and code actions (#1376)
Fixes #1375 Fixes #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 <richard.willis@chevinfleet.com> Co-authored-by: Lasath Fernando <devel@lasath.org>
1 parent 88e3d9b commit 84b775f

File tree

9 files changed

+527
-84
lines changed

9 files changed

+527
-84
lines changed

Src/CSharpier.VSCode/README.md

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -72,16 +72,3 @@ dotnet tool install csharpier
7272

7373
# rebuild container image
7474
```
75-
76-
## Limitations
77-
78-
Format Selection is not supported.
79-
80-
Only `"editor.formatOnSaveMode" : "file"` is supported. If using other modes, you can set `file` by scoping the setting:
81-
```json
82-
"editor.formatOnSave": true,
83-
"editor.formatOnSaveMode": "modifications"
84-
"[csharp]": {
85-
"editor.formatOnSaveMode": "file"
86-
}
87-
```

Src/CSharpier.VSCode/package-lock.json

Lines changed: 46 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Src/CSharpier.VSCode/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
"@types/mocha": "9.0.0",
7676
"@types/node": "14.x",
7777
"@types/node-fetch": "^2.6.11",
78+
"@types/prettier-linter-helpers": "^1.0.4",
7879
"@types/semver": "7.3.9",
7980
"@types/vscode": "1.60.0",
8081
"prettier": "2.4.1",
@@ -88,6 +89,7 @@
8889
"xml-js": "1.6.11"
8990
},
9091
"dependencies": {
91-
"node-fetch": "^2.7.0"
92+
"node-fetch": "^2.7.0",
93+
"prettier-linter-helpers": "^1.0.0"
9294
}
9395
}
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import * as vscode from "vscode";
2+
import { Difference, generateDifferences, showInvisibles } from "prettier-linter-helpers";
3+
import { FixAllCodeActionsCommand } from "./FixAllCodeActionCommand";
4+
import { Logger } from "./Logger";
5+
import { FormatDocumentProvider } from "./FormatDocumentProvider";
6+
7+
const DIAGNOSTICS_ID = "csharpier";
8+
const DIAGNOSTICS_SOURCE_ID = "diagnostic";
9+
10+
export interface CsharpierDiff {
11+
source: string;
12+
formattedSource: string;
13+
differences: Difference[];
14+
}
15+
16+
export class DiagnosticsService implements vscode.CodeActionProvider, vscode.Disposable {
17+
public static readonly quickFixCodeActionKind =
18+
vscode.CodeActionKind.QuickFix.append(DIAGNOSTICS_ID);
19+
public static metadata: vscode.CodeActionProviderMetadata = {
20+
providedCodeActionKinds: [DiagnosticsService.quickFixCodeActionKind],
21+
};
22+
23+
private readonly diagnosticCollection: vscode.DiagnosticCollection;
24+
private readonly diagnosticDifferenceMap: Map<vscode.Diagnostic, Difference> = new Map();
25+
private readonly codeActionsProvider: vscode.Disposable;
26+
private readonly disposables: vscode.Disposable[] = [];
27+
28+
constructor(
29+
private readonly formatDocumentProvider: FormatDocumentProvider,
30+
private readonly documentSelector: Array<vscode.DocumentFilter>,
31+
private readonly logger: Logger,
32+
) {
33+
this.diagnosticCollection = vscode.languages.createDiagnosticCollection(DIAGNOSTICS_ID);
34+
this.codeActionsProvider = vscode.languages.registerCodeActionsProvider(
35+
this.documentSelector,
36+
this,
37+
DiagnosticsService.metadata,
38+
);
39+
this.registerEditorEvents();
40+
}
41+
42+
public dispose(): void {
43+
for (const disposable of this.disposables) {
44+
disposable.dispose();
45+
}
46+
this.diagnosticCollection.dispose();
47+
this.codeActionsProvider.dispose();
48+
}
49+
50+
private handleChangeTextDocument(document: vscode.TextDocument): void {
51+
void this.runDiagnostics(document);
52+
}
53+
54+
public async runDiagnostics(document: vscode.TextDocument): Promise<void> {
55+
const shouldRunDiagnostics =
56+
this.documentSelector.some(selector => selector.language === document.languageId) &&
57+
!!vscode.workspace.getWorkspaceFolder(document.uri);
58+
if (shouldRunDiagnostics) {
59+
try {
60+
const diff = await this.getDiff(document);
61+
this.updateDiagnostics(document, diff);
62+
} catch (e) {
63+
this.logger.error(`Unable to provide diagnostics: ${(e as Error).message}`);
64+
}
65+
}
66+
}
67+
68+
public updateDiagnostics(document: vscode.TextDocument, diff: CsharpierDiff): void {
69+
const diagnostics = this.getDiagnostics(document, diff);
70+
this.diagnosticCollection.set(document.uri, diagnostics);
71+
}
72+
73+
private registerEditorEvents(): void {
74+
const activeDocument = vscode.window.activeTextEditor?.document;
75+
if (activeDocument) {
76+
void this.runDiagnostics(activeDocument);
77+
}
78+
79+
const onDidChangeTextDocument = vscode.workspace.onDidChangeTextDocument(
80+
(e: vscode.TextDocumentChangeEvent) => {
81+
if (
82+
e.contentChanges.length &&
83+
vscode.window.activeTextEditor?.document === e.document
84+
) {
85+
this.handleChangeTextDocument(e.document);
86+
}
87+
},
88+
);
89+
90+
const onDidChangeActiveTextEditor = vscode.window.onDidChangeActiveTextEditor(
91+
(editor?: vscode.TextEditor) => {
92+
if (editor) {
93+
void this.runDiagnostics(editor.document);
94+
}
95+
},
96+
);
97+
98+
this.disposables.push(
99+
onDidChangeTextDocument,
100+
onDidChangeActiveTextEditor,
101+
this.diagnosticCollection,
102+
);
103+
}
104+
105+
private getDiagnostics(
106+
document: vscode.TextDocument,
107+
diff: CsharpierDiff,
108+
): vscode.Diagnostic[] {
109+
const diagnostics: vscode.Diagnostic[] = [];
110+
for (const difference of diff.differences) {
111+
const diagnostic = this.getDiagnostic(document, difference);
112+
this.diagnosticDifferenceMap.set(diagnostic, difference);
113+
diagnostics.push(diagnostic);
114+
}
115+
return diagnostics;
116+
}
117+
118+
private getDiagnostic(
119+
document: vscode.TextDocument,
120+
difference: Difference,
121+
): vscode.Diagnostic {
122+
const range = this.getRange(document, difference);
123+
const message = this.getMessage(difference);
124+
const diagnostic = new vscode.Diagnostic(range, message);
125+
diagnostic.source = DIAGNOSTICS_ID;
126+
diagnostic.code = DIAGNOSTICS_SOURCE_ID;
127+
return diagnostic;
128+
}
129+
130+
private getMessage(difference: Difference): string {
131+
switch (difference.operation) {
132+
case generateDifferences.INSERT:
133+
return `Insert ${showInvisibles(difference.insertText!)}`;
134+
case generateDifferences.REPLACE:
135+
return `Replace ${showInvisibles(difference.deleteText!)} with ${showInvisibles(
136+
difference.insertText!,
137+
)}`;
138+
case generateDifferences.DELETE:
139+
return `Delete ${showInvisibles(difference.deleteText!)}`;
140+
default:
141+
return "";
142+
}
143+
}
144+
145+
private getRange(document: vscode.TextDocument, difference: Difference): vscode.Range {
146+
if (difference.operation === generateDifferences.INSERT) {
147+
const start = document.positionAt(difference.offset);
148+
return new vscode.Range(start.line, start.character, start.line, start.character);
149+
}
150+
const start = document.positionAt(difference.offset);
151+
const end = document.positionAt(difference.offset + difference.deleteText!.length);
152+
return new vscode.Range(start.line, start.character, end.line, end.character);
153+
}
154+
155+
private async getDiff(document: vscode.TextDocument): Promise<CsharpierDiff> {
156+
const source = document.getText();
157+
const formattedSource =
158+
(await this.formatDocumentProvider.formatDocument(document)) ?? source;
159+
const differences = generateDifferences(source, formattedSource);
160+
return {
161+
source,
162+
formattedSource,
163+
differences,
164+
};
165+
}
166+
167+
public provideCodeActions(
168+
document: vscode.TextDocument,
169+
range: vscode.Range | vscode.Selection,
170+
): vscode.CodeAction[] {
171+
let totalDiagnostics = 0;
172+
const codeActions: vscode.CodeAction[] = [];
173+
this.diagnosticCollection.forEach(
174+
(uri: vscode.Uri, diagnostics: readonly vscode.Diagnostic[]) => {
175+
if (document.uri.fsPath !== uri.fsPath) {
176+
return;
177+
}
178+
diagnostics.forEach((diagnostic: vscode.Diagnostic) => {
179+
totalDiagnostics += 1;
180+
if (!range.isEqual(diagnostic.range)) {
181+
return;
182+
}
183+
const difference = this.diagnosticDifferenceMap.get(diagnostic);
184+
codeActions.push(
185+
this.getQuickFixCodeAction(document.uri, diagnostic, difference!),
186+
);
187+
});
188+
},
189+
);
190+
if (totalDiagnostics > 1) {
191+
codeActions.push(this.getQuickFixAllProblemsCodeAction(document, totalDiagnostics));
192+
}
193+
return codeActions;
194+
}
195+
196+
private getQuickFixCodeAction(
197+
uri: vscode.Uri,
198+
diagnostic: vscode.Diagnostic,
199+
difference: Difference,
200+
): vscode.CodeAction {
201+
const action = new vscode.CodeAction(
202+
`Fix this ${DIAGNOSTICS_ID} problem`,
203+
DiagnosticsService.quickFixCodeActionKind,
204+
);
205+
action.edit = new vscode.WorkspaceEdit();
206+
if (difference.operation === generateDifferences.INSERT) {
207+
action.edit.insert(uri, diagnostic.range.start, difference.insertText!);
208+
} else if (difference.operation === generateDifferences.REPLACE) {
209+
action.edit.replace(uri, diagnostic.range, difference.insertText!);
210+
} else if (difference.operation === generateDifferences.DELETE) {
211+
action.edit.delete(uri, diagnostic.range);
212+
}
213+
return action;
214+
}
215+
216+
private getQuickFixAllProblemsCodeAction(
217+
document: vscode.TextDocument,
218+
totalDiagnostics: number,
219+
): vscode.CodeAction {
220+
const title = `Fix all ${DIAGNOSTICS_ID} problems (${totalDiagnostics})`;
221+
const action = new vscode.CodeAction(title, DiagnosticsService.quickFixCodeActionKind);
222+
action.command = {
223+
title,
224+
command: FixAllCodeActionsCommand.Id,
225+
arguments: [document],
226+
};
227+
return action;
228+
}
229+
}

0 commit comments

Comments
 (0)