|
| 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