diff --git a/package.json b/package.json index bc258db7..8fc92c17 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,12 @@ "icon": "icon.png", "main": "./dist/extension.js", "contributes": { + "commands": [ + { + "command": "vscode-blade-formatter.format", + "title": "Blade: Format Document" + } + ], "jsonValidation": [ { "fileMatch": ".bladeformatterrc.json", diff --git a/src/commands.ts b/src/commands.ts new file mode 100644 index 00000000..fef8323c --- /dev/null +++ b/src/commands.ts @@ -0,0 +1,40 @@ +import vscode, { commands, TextEditor, TextEditorEdit, window, WorkspaceConfiguration } from "vscode"; +import { readRuntimeConfig } from './runtimeConfig'; +import { getCoreNodeModule } from "./util"; +import { Formatter } from "blade-formatter"; +import { Logger } from "./logger"; + +const vsctmModule = getCoreNodeModule("vscode-textmate"); +const onigurumaModule = getCoreNodeModule("vscode-oniguruma"); +const { Range, Position } = vscode; + +export const formatFromCommand = + async function (editor: TextEditor, edit: TextEditorEdit) { + try { + const extConfig = vscode.workspace.getConfiguration( + "bladeFormatter.format" + ); + const runtimeConfig = readRuntimeConfig(editor.document.uri.fsPath); + const options = { + vsctm: vsctmModule, + oniguruma: onigurumaModule, + indentSize: extConfig.indentSize, + wrapLineLength: extConfig.wrapLineLength, + wrapAttributes: extConfig.wrapAttributes, + useTabs: extConfig.useTabs, + sortTailwindcssClasses: extConfig.sortTailwindcssClasses, + ...runtimeConfig, + }; + const originalText = editor.document.getText(); + const lastLine = editor.document.lineAt(editor.document.lineCount - 1); + const range = new Range(new Position(0, 0), lastLine.range.end); + const formatted = await new Formatter(options).formatContent(originalText); + + await editor.edit((editBuilder) => { + editBuilder.replace(range, formatted); + }); + } catch (e) { + const logger = new Logger(); + logger.logError("Error formatting document", e); + } + } diff --git a/src/constants.ts b/src/constants.ts index 8a5c43f7..d88814fb 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -3,4 +3,5 @@ export const enum ExtensionConstants { firstActivationStorageKey = 'firstActivation', globalVersionKey = 'vscode-blade-formatter-version', displayName = 'Laravel Blade Formatter', + formatCommandKey = 'vscode-blade-formatter.format', } diff --git a/src/extension.ts b/src/extension.ts index 7a489d00..407ae028 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,4 +1,4 @@ -import vscode, { WorkspaceConfiguration } from "vscode"; +import vscode, { commands, window, WorkspaceConfiguration } from "vscode"; import { ExtensionContext } from "vscode"; import path from "path"; import findConfig from "find-config"; @@ -10,6 +10,7 @@ import { telemetry, TelemetryEventNames } from './telemetry'; import { readRuntimeConfig } from './runtimeConfig'; import { ExtensionConstants } from "./constants"; import { messages } from "./messages"; +import { formatFromCommand } from "./commands"; import { getCoreNodeModule } from "./util"; const { Range, Position } = vscode; @@ -41,6 +42,11 @@ export function activate(context: ExtensionContext) { context.globalState.update(ExtensionConstants.firstActivationStorageKey, false); } + commands.registerTextEditorCommand( + ExtensionConstants.formatCommandKey, + formatFromCommand + ); + context.subscriptions.push( vscode.languages.registerDocumentFormattingEditProvider("blade", { provideDocumentFormattingEdits(document: vscode.TextDocument, vscodeOpts: vscode.FormattingOptions): any { diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 00000000..37660d51 --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,108 @@ +import { window } from "vscode"; + +type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR" | "NONE"; + +export class Logger { + private outputChannel = window.createOutputChannel("BladeFormatter"); + + private logLevel: LogLevel = "INFO"; + + public setOutputLevel(logLevel: LogLevel) { + this.logLevel = logLevel; + } + + /** + * Append messages to the output channel and format it with a title + * + * @param message The message to append to the output channel + */ + public logDebug(message: string, data?: unknown): void { + if ( + this.logLevel === "NONE" || + this.logLevel === "INFO" || + this.logLevel === "WARN" || + this.logLevel === "ERROR" + ) { + return; + } + this.logMessage(message, "DEBUG"); + if (data) { + this.logObject(data); + } + } + + /** + * Append messages to the output channel and format it with a title + * + * @param message The message to append to the output channel + */ + public logInfo(message: string, data?: unknown): void { + if ( + this.logLevel === "NONE" || + this.logLevel === "WARN" || + this.logLevel === "ERROR" + ) { + return; + } + this.logMessage(message, "INFO"); + if (data) { + this.logObject(data); + } + } + + /** + * Append messages to the output channel and format it with a title + * + * @param message The message to append to the output channel + */ + public logWarning(message: string, data?: unknown): void { + if (this.logLevel === "NONE" || this.logLevel === "ERROR") { + return; + } + this.logMessage(message, "WARN"); + if (data) { + this.logObject(data); + } + } + + public logError(message: string, error?: unknown) { + if (this.logLevel === "NONE") { + return; + } + this.logMessage(message, "ERROR"); + if (typeof error === "string") { + // Errors as a string usually only happen with + // plugins that don't return the expected error. + this.outputChannel.appendLine(error); + } else if (error instanceof Error) { + if (error?.message) { + this.logMessage(error.message, "ERROR"); + } + if (error?.stack) { + this.outputChannel.appendLine(error.stack); + } + } else if (error) { + this.logObject(error); + } + } + + public show() { + this.outputChannel.show(); + } + + private logObject(data: unknown): void { + const message = JSON.stringify(data, null, 2); // dont use prettier to keep it simple + + this.outputChannel.appendLine(message); + } + + /** + * Append messages to the output channel and format it with a title + * + * @param message The message to append to the output channel + */ + private logMessage(message: string, logLevel: LogLevel): void { + const title = new Date().toLocaleTimeString(); + this.outputChannel.appendLine(`["${logLevel}" - ${title}] ${message}`); + } +}