From 89e077f4e92481ecfe8c63ccd99a40d17ecca660 Mon Sep 17 00:00:00 2001 From: Richard Willis Date: Sun, 17 Nov 2024 18:33:36 +0000 Subject: [PATCH] VSCode extension: add format selection support --- .../src/DiagnosticsService.ts | 19 +-- Src/CSharpier.VSCode/src/Extension.ts | 6 +- .../src/FormatDocumentProvider.ts | 64 +++++++++ Src/CSharpier.VSCode/src/FormattingService.ts | 133 +++++++++--------- 4 files changed, 142 insertions(+), 80 deletions(-) create mode 100644 Src/CSharpier.VSCode/src/FormatDocumentProvider.ts diff --git a/Src/CSharpier.VSCode/src/DiagnosticsService.ts b/Src/CSharpier.VSCode/src/DiagnosticsService.ts index ea8a87e12..c28dc8c76 100644 --- a/Src/CSharpier.VSCode/src/DiagnosticsService.ts +++ b/Src/CSharpier.VSCode/src/DiagnosticsService.ts @@ -3,6 +3,7 @@ import { Difference, generateDifferences, showInvisibles } from "prettier-linter import { FixAllCodeActionsCommand } from "./FixAllCodeActionCommand"; import { CSharpierProcessProvider } from "./CSharpierProcessProvider"; import { Logger } from "./Logger"; +import { FormatDocumentProvider } from "./FormatDocumentProvider"; const DIAGNOSTICS_ID = "csharpier"; const DIAGNOSTICS_SOURCE_ID = "diagnostic"; @@ -26,7 +27,7 @@ export class DiagnosticsService implements vscode.CodeActionProvider, vscode.Dis private readonly disposables: vscode.Disposable[] = []; constructor( - private readonly csharpierProcessProvider: CSharpierProcessProvider, + private readonly formatDocumentProvider: FormatDocumentProvider, private readonly documentSelector: Array, private readonly logger: Logger, ) { @@ -154,20 +155,8 @@ export class DiagnosticsService implements vscode.CodeActionProvider, vscode.Dis private async getDiff(document: vscode.TextDocument): Promise { const source = document.getText(); - const csharpierProcess = this.csharpierProcessProvider.getProcessFor(document.fileName); - let formattedSource = ""; - if ("formatFile2" in csharpierProcess) { - const parameter = { - fileContents: source, - fileName: document.fileName, - }; - const result = await csharpierProcess.formatFile2(parameter); - if (result) { - formattedSource = result.formattedFile; - } - } else { - formattedSource = await csharpierProcess.formatFile(source, document.fileName); - } + const formattedSource = + (await this.formatDocumentProvider.formatDocument(document)) ?? source; const differences = generateDifferences(source, formattedSource); return { source, diff --git a/Src/CSharpier.VSCode/src/Extension.ts b/Src/CSharpier.VSCode/src/Extension.ts index 694c6bd00..7bdebbf7e 100644 --- a/Src/CSharpier.VSCode/src/Extension.ts +++ b/Src/CSharpier.VSCode/src/Extension.ts @@ -8,6 +8,7 @@ import { NullCSharpierProcess } from "./NullCSharpierProcess"; import { FixAllCodeActionsCommand } from "./FixAllCodeActionCommand"; import { DiagnosticsService } from "./DiagnosticsService"; import { FixAllCodeActionProvider } from "./FixAllCodeActionProvider"; +import { FormatDocumentProvider } from "./FormatDocumentProvider"; export async function activate(context: ExtensionContext) { if (!workspace.isTrusted) { @@ -40,6 +41,7 @@ const initPlugin = async (context: ExtensionContext) => { NullCSharpierProcess.create(logger); const csharpierProcessProvider = new CSharpierProcessProvider(logger, context.extension); + const formatDocumentProvider = new FormatDocumentProvider(logger, csharpierProcessProvider); const diagnosticsDocumentSelector: DocumentFilter[] = [ { language: "csharp", @@ -47,13 +49,13 @@ const initPlugin = async (context: ExtensionContext) => { }, ]; const diagnosticsService = new DiagnosticsService( - csharpierProcessProvider, + formatDocumentProvider, diagnosticsDocumentSelector, logger, ); const fixAllCodeActionProvider = new FixAllCodeActionProvider(diagnosticsDocumentSelector); - new FormattingService(logger, csharpierProcessProvider); + new FormattingService(formatDocumentProvider); new FixAllCodeActionsCommand(context, csharpierProcessProvider, logger); context.subscriptions.push( diff --git a/Src/CSharpier.VSCode/src/FormatDocumentProvider.ts b/Src/CSharpier.VSCode/src/FormatDocumentProvider.ts new file mode 100644 index 000000000..0c7fed3f7 --- /dev/null +++ b/Src/CSharpier.VSCode/src/FormatDocumentProvider.ts @@ -0,0 +1,64 @@ +import { TextDocument } from "vscode"; +import { Logger } from "./Logger"; +import { CSharpierProcessProvider } from "./CSharpierProcessProvider"; +import { performance } from "perf_hooks"; + +export class FormatDocumentProvider { + constructor( + private logger: Logger, + private csharpierProcessProvider: CSharpierProcessProvider, + ) {} + + async formatDocument(document: TextDocument): Promise { + const csharpierProcess = this.csharpierProcessProvider.getProcessFor(document.fileName); + const text = document.getText(); + const startTime = performance.now(); + + if ("formatFile2" in csharpierProcess) { + const parameter = { + fileContents: text, + fileName: document.fileName, + }; + const result = await csharpierProcess.formatFile2(parameter); + + this.logger.info("Formatted in " + (performance.now() - startTime) + "ms"); + + if (result == null) { + return null; + } + + switch (result.status) { + case "Formatted": + return result.formattedFile; + case "Ignored": + this.logger.info("File is ignored by csharpier cli."); + break; + case "Failed": + this.logger.warn( + "CSharpier cli failed to format the file and returned the following error: " + + result.errorMessage, + ); + break; + default: + this.logger.warn("Didn't handle " + result.status); + break; + } + } else { + const newText = await csharpierProcess.formatFile(text, document.fileName); + const endTime = performance.now(); + this.logger.info("Formatted in " + (endTime - startTime) + "ms"); + if (!newText || newText === text) { + this.logger.debug( + "Skipping write because " + !newText + ? "result is empty" + : "current document equals result", + ); + return null; + } + + return newText; + } + + return null; + } +} diff --git a/Src/CSharpier.VSCode/src/FormattingService.ts b/Src/CSharpier.VSCode/src/FormattingService.ts index c88a57508..add4b3723 100644 --- a/Src/CSharpier.VSCode/src/FormattingService.ts +++ b/Src/CSharpier.VSCode/src/FormattingService.ts @@ -1,81 +1,88 @@ import { performance } from "perf_hooks"; -import { languages, Range, TextDocument, TextEdit } from "vscode"; -import { CSharpierProcessProvider } from "./CSharpierProcessProvider"; -import { Logger } from "./Logger"; -import { Status } from "./ICSharpierProcess"; +import { + CancellationToken, + Diagnostic, + FormattingOptions, + languages, + Position, + Range, + TextDocument, + TextEdit, + WorkspaceEdit, +} from "vscode"; +import { Difference, generateDifferences } from "prettier-linter-helpers"; +import { FormatDocumentProvider } from "./FormatDocumentProvider"; export class FormattingService { - logger: Logger; - csharpierProcessProvider: CSharpierProcessProvider; - - constructor(logger: Logger, csharpierProcessProvider: CSharpierProcessProvider) { - this.logger = logger; - this.csharpierProcessProvider = csharpierProcessProvider; - + constructor(private readonly formatDocumentProvider: FormatDocumentProvider) { languages.registerDocumentFormattingEditProvider("csharp", { provideDocumentFormattingEdits: this.provideDocumentFormattingEdits, }); + + languages.registerDocumentRangeFormattingEditProvider("csharp", { + provideDocumentRangeFormattingEdits: this.provideDocumentRangeFormattingEdits, + }); } - private provideDocumentFormattingEdits = async (document: TextDocument) => { - const csharpierProcess = this.csharpierProcessProvider.getProcessFor(document.fileName); + private provideDocumentRangeFormattingEdits = async ( + document: TextDocument, + range: Range, + ): Promise => { + const differences = await this.getDifferences(document); + const edits: TextEdit[] = []; + + for (const difference of differences) { + const diffRange = this.getRange(document, difference); + if (range.contains(diffRange)) { + const textEdit = this.getTextEdit(diffRange, difference); + if (textEdit) { + edits.push(textEdit); + } + } + } - this.logger.info( - "Formatting started for " + - document.fileName + - " using CSharpier " + - csharpierProcess.getVersion(), - ); - const startTime = performance.now(); - const text = document.getText(); + return edits; + }; - const updateText = (newText: string) => { - return [TextEdit.replace(FormattingService.fullDocumentRange(document), newText)]; - }; + private getTextEdit(range: Range, difference: Difference) { + if (difference.operation === generateDifferences.INSERT) { + return TextEdit.insert( + new Position(range.start.line, range.start.character), + difference.insertText!, + ); + } else if (difference.operation === generateDifferences.REPLACE) { + return TextEdit.replace(range, difference.insertText!); + } else if (difference.operation === generateDifferences.DELETE) { + return TextEdit.delete(range); + } + } - if ("formatFile2" in csharpierProcess) { - const parameter = { - fileContents: text, - fileName: document.fileName, - }; - const result = await csharpierProcess.formatFile2(parameter); + private async getDifferences(document: TextDocument) { + const source = document.getText(); + const formattedSource = + (await this.formatDocumentProvider.formatDocument(document)) ?? source; + return generateDifferences(source, formattedSource); + } - this.logger.info("Formatted in " + (performance.now() - startTime) + "ms"); + private getRange(document: TextDocument, difference: Difference): Range { + if (difference.operation === generateDifferences.INSERT) { + const start = document.positionAt(difference.offset); + return new 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 Range(start.line, start.character, end.line, end.character); + } - if (result == null) { - return; - } + private provideDocumentFormattingEdits = async (document: TextDocument) => { + const updateText = (newText: string) => { + return [TextEdit.replace(FormattingService.fullDocumentRange(document), newText)]; + }; - switch (result.status) { - case "Formatted": - return updateText(result.formattedFile); - case "Ignored": - this.logger.info("File is ignored by csharpier cli."); - break; - case "Failed": - this.logger.warn( - "CSharpier cli failed to format the file and returned the following error: " + - result.errorMessage, - ); - break; - default: - this.logger.warn("Didn't handle " + result.status); - break; - } - } else { - const newText = await csharpierProcess.formatFile(text, document.fileName); - const endTime = performance.now(); - this.logger.info("Formatted in " + (endTime - startTime) + "ms"); - if (!newText || newText === text) { - this.logger.debug( - "Skipping write because " + !newText - ? "result is empty" - : "current document equals result", - ); - return []; - } + const formattedSource = await this.formatDocumentProvider.formatDocument(document); - return updateText(newText); + if (formattedSource) { + return updateText(formattedSource); } return [];