From 595bb8194e40389ccb967fbd533b7b22bd29b3c6 Mon Sep 17 00:00:00 2001 From: Grol Grol Date: Wed, 14 May 2025 13:24:08 +0300 Subject: [PATCH] feat(settings): Add option to enable/disable debug logging --- docs/settings.md | 16 ++- esbuild-debug-wrap-plugin.mjs | 78 +++++++++++++ esbuild.config.mjs | 7 ++ package-lock.json | 2 +- src/main.ts | 103 ++++++++++-------- src/types.ts | 4 +- src/ui/CheckboxSyncPluginSettingTab.ts | 13 ++- src/ui/components/devGroup/EnableDebug.ts | 57 ++++++++++ .../EnableChildSyncSettingComponent.ts | 2 +- 9 files changed, 232 insertions(+), 50 deletions(-) create mode 100644 esbuild-debug-wrap-plugin.mjs create mode 100644 src/ui/components/devGroup/EnableDebug.ts diff --git a/docs/settings.md b/docs/settings.md index 5c61036..3f3474d 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -51,6 +51,13 @@ These settings control when and how the synchronization logic runs. - **Enabled:** Automatically synchronizes checkbox states when files are loaded/opened and immediately after plugin settings are applied. This ensures consistency but might have performance implications on very large vaults or files. *(Requires Obsidian restart or settings reload to take full effect)*. - **Disabled (Default):** Synchronization only occurs when you *manually* change a checkbox's state within Obsidian. This is the default behavior to minimize potential performance impact. +### Dev + +- **Enable console log** + - **Description:** Toggles detailed logging to the developer console. When enabled, the plugin will output more information about its operations, which can be helpful for troubleshooting or understanding its behavior. + - **Enabled:** Debug logs are printed to the console. + - **Disabled (Default):** Debug logs are suppressed. + ### Actions and Status - **Error Display:** An area below the settings displays any errors encountered, such as invalid JSON in the symbol configuration. @@ -105,10 +112,17 @@ These settings control when and how the synchronization logic runs. - **Включено:** Автоматически синхронизирует состояния чекбоксов при загрузке/открытии файлов и сразу после применения настроек плагина. Это обеспечивает консистентность, но может влиять на производительность в очень больших хранилищах или файлах. *(Требует перезапуска Obsidian или перезагрузки настроек для полного вступления в силу)*. - **Отключено (По умолчанию):** Синхронизация происходит только тогда, когда вы *вручную* изменяете состояние чекбокса в Obsidian. Это поведение по умолчанию для минимизации потенциального влияния на производительность. +### Разработка / Отладка + +- **Включить Отладочное Логирование (Enable console log)** + - **Описание:** Переключает вывод подробных логов в консоль разработчика. Когда включено, плагин будет выводить больше информации о своих операциях, что может быть полезно для устранения неполадок или понимания его поведения. + - **Включено:** Отладочные логи выводятся в консоль. + - **Отключено (По умолчанию):** Отладочные логи не выводятся. + ### Действия и Статус - **Отображение Ошибок:** Область под настройками отображает любые возникшие ошибки, например, невалидный JSON в конфигурации символов. - **Кнопки:** - `Apply Changes` (Применить изменения): Сохраняет и применяет измененные настройки. Активна только при наличии изменений. - `Reset changes` (Отменить изменения): Возвращает несохраненные изменения к последнему примененному состоянию настроек. - - `Reset to defaults` (Сбросить по умолчанию): Сбрасывает все настройки к значениям по умолчанию и немедленно применяет их. \ No newline at end of file + - `Reset to defaults` (Сбросить по умолчанию): Сбрасывает все настройки к значениям по умолчанию и немедленно применяет их. diff --git a/esbuild-debug-wrap-plugin.mjs b/esbuild-debug-wrap-plugin.mjs new file mode 100644 index 0000000..c248ea8 --- /dev/null +++ b/esbuild-debug-wrap-plugin.mjs @@ -0,0 +1,78 @@ +import { promises } from "fs"; + +/** + * Плагин esbuild для обертывания вызовов console.* в условие. + * @param {object} options + * @param {string} [options.debugFlagName='DEBUG_WRAP_CONSOLE_PLUGIN_FLAG'] - Имя глобальной переменной для проверки. + * @param {string[]} [options.methods=['log', 'warn', 'info', 'debug', 'error']] - Методы console, которые нужно обернуть. + * @returns {import('esbuild').Plugin} + */ +export const debugWrapConsolePlugin = ({ + debugFlagName = "DEBUG_WRAP_CONSOLE_PLUGIN_FLAG", // Имя глобального флага (лучше сделать уникальным для вашего плагина) + methods = ["log", "warn", "info", "debug", "error"], // Какие методы console оборачивать +} = {}) => ({ + name: "debug-wrap-console", + setup(build) { + // Создаем фильтр для методов console + const methodsPattern = methods.join("|"); + // Регулярное выражение для поиска вызовов console.method(...) + // Оно пытается обработать простые случаи, но может быть неидеальным для сложных вложенных выражений или многострочных вызовов. + // $& в замене представляет всю совпавшую строку. + const consoleRegex = new RegExp( + // Match 'console.' followed by one of the specified methods + `(^|\\s+|\\{|\\;)(console\\.(${methodsPattern}))\\s*\\(` + + // Match arguments (non-greedy) - this is the tricky part and might not capture perfectly balanced parentheses in all complex cases + `([\\s\\S]*?)` + + // Match the closing parenthesis and optional semicolon + `\\)(;?)`, + "g" // Global search + ); + + const wrapperStart = `if (window.${debugFlagName}) { `; + const wrapperEnd = ` }`; + + // Перехватываем загрузку JS/TS файлов + build.onLoad({ filter: /\.[jt]sx?$/ }, async (args) => { + try { + // Читаем содержимое файла + const source = await promises.readFile(args.path, "utf8"); + + // Заменяем вызовы console.* + const contents = source.replace( + consoleRegex, + (match, prefix, fullCall, methodName, argsContent, semicolon) => { + // prefix: Пробел, начало строки, {, ; перед вызовом console + // fullCall: Сам вызов, например, console.log + // methodName: Имя метода, например, log + // argsContent: Содержимое скобок + // semicolon: Завершающая точка с запятой (если была) + + // Собираем обернутый вызов + // Мы используем 'match' целиком, чтобы сохранить оригинальное форматирование и содержимое, + // но убираем исходный префикс (пробел/начало строки/итд) и добавляем его перед if. + const originalCall = match.substring(prefix.length); + return `${prefix}${wrapperStart}${originalCall}${wrapperEnd}`; + } + ); + + // Возвращаем измененное содержимое и указываем esbuild, + // что это все еще JS/TS код (в зависимости от исходного файла) + const loader = + args.path.endsWith(".ts") || args.path.endsWith(".tsx") ? "ts" : "js"; + + return { + contents, + loader, + }; + } catch (error) { + console.error( + `Error processing file ${args.path} in debug-wrap-console plugin:`, + error + ); + // В случае ошибки возвращаем null или выбрасываем ошибку, чтобы сборка прервалась + return null; + } + }); + }, +}); + diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 54986fd..3d62642 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -1,6 +1,7 @@ import esbuild from "esbuild"; import process from "process"; import builtins from "builtin-modules"; +import { debugWrapConsolePlugin } from "./esbuild-debug-wrap-plugin.mjs"; const banner = `/* @@ -39,6 +40,12 @@ const context = await esbuild.context({ treeShaking: true, outfile: "main.js", minify: prod, + plugins: [ + debugWrapConsolePlugin({ + debugFlagName: 'CHECKBOX_SYNC_DEBUG', + methods: ['log', 'info', 'debug'], + }), + ], }); if (prod) { diff --git a/package-lock.json b/package-lock.json index ac7b798..ce0380a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,7 @@ "": { "name": "checkbox-sync", "version": "1.0.0", - "license": "MIT", + "license": "0BSD", "dependencies": { "async-mutex": "^0.5.0" }, diff --git a/src/main.ts b/src/main.ts index a23c16d..ecb68a5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,49 +7,64 @@ import { CheckboxSyncPluginSettingTab } from "./ui/CheckboxSyncPluginSettingTab" import SyncController from "./SyncController"; import { CheckboxSyncPluginSettings, DEFAULT_SETTINGS } from "./types"; -export default class CheckboxSyncPlugin extends Plugin { - private _settings: CheckboxSyncPluginSettings; +const DEBUG_FLAG_NAME = 'CHECKBOX_SYNC_DEBUG'; - private syncController: SyncController; - private checkboxUtils: CheckboxUtils; - private fileStateHolder: FileStateHolder; - private fileLoadEventHandler: FileLoadEventHandler; - private fileChangeEventHandler: FileChangeEventHandler; - - async onload() { - await this.loadSettings(); - - this.fileStateHolder = new FileStateHolder(this.app.vault); - this.checkboxUtils = new CheckboxUtils(this.settings); - this.syncController = new SyncController(this.app.vault, this.checkboxUtils, this.fileStateHolder); - this.fileLoadEventHandler = new FileLoadEventHandler(this, this.app, this.syncController, this.fileStateHolder); - this.fileChangeEventHandler = new FileChangeEventHandler(this, this.app, this.syncController, this.fileStateHolder); - - this.addSettingTab(new CheckboxSyncPluginSettingTab(this.app, this)); - this.fileLoadEventHandler.registerEvents(); - this.fileChangeEventHandler.registerEvents(); - } - - async loadSettings() { - this._settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); - } - - get settings(): Readonly { - return this._settings; - } - - async updateSettings(callback: (settings: CheckboxSyncPluginSettings) => void | Promise) { - await callback(this._settings); - await this.saveData(this.settings); - - if (this.settings.enableAutomaticFileSync) { - //надо пересинхронизировать все файлы в кеше - const allFile = this.fileStateHolder.getAllFiles(); - await Promise.all( - allFile.map(async (file: TFile) => { - await this.syncController.syncFile(file); - }) - ); - } - } +// --- Объявление глобальной переменной для TypeScript --- +// Это нужно, чтобы TypeScript не ругался на window[DEBUG_FLAG_NAME] +declare global { + interface Window { + [key: string]: any; // Позволяем индексировать window строкой + } +} + +export default class CheckboxSyncPlugin extends Plugin { + private _settings: CheckboxSyncPluginSettings; + + private syncController: SyncController; + private checkboxUtils: CheckboxUtils; + private fileStateHolder: FileStateHolder; + private fileLoadEventHandler: FileLoadEventHandler; + private fileChangeEventHandler: FileChangeEventHandler; + + async onload() { + await this.loadSettings(); + + this.fileStateHolder = new FileStateHolder(this.app.vault); + this.checkboxUtils = new CheckboxUtils(this.settings); + this.syncController = new SyncController(this.app.vault, this.checkboxUtils, this.fileStateHolder); + this.fileLoadEventHandler = new FileLoadEventHandler(this, this.app, this.syncController, this.fileStateHolder); + this.fileChangeEventHandler = new FileChangeEventHandler(this, this.app, this.syncController, this.fileStateHolder); + + this.addSettingTab(new CheckboxSyncPluginSettingTab(this.app, this)); + this.fileLoadEventHandler.registerEvents(); + this.fileChangeEventHandler.registerEvents(); + } + + async loadSettings() { + this._settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + } + + get settings(): Readonly { + return this._settings; + } + + async updateSettings(callback: (settings: CheckboxSyncPluginSettings) => void | Promise) { + await callback(this._settings); + this.setDebugFlag(this.settings.consoleEnabled); + await this.saveData(this.settings); + + if (this.settings.enableAutomaticFileSync) { + //надо пересинхронизировать все файлы в кеше + const allFile = this.fileStateHolder.getAllFiles(); + await Promise.all( + allFile.map(async (file: TFile) => { + await this.syncController.syncFile(file); + }) + ); + } + } + + private setDebugFlag(enabled: boolean) { + window[DEBUG_FLAG_NAME] = enabled; + } } diff --git a/src/types.ts b/src/types.ts index 709bee8..77c49dc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,6 +6,7 @@ export interface CheckboxSyncPluginSettings { ignoreSymbols: string[]; unknownSymbolPolicy: CheckboxState; enableAutomaticFileSync: boolean; + consoleEnabled: boolean; } export enum CheckboxState { @@ -22,4 +23,5 @@ export const DEFAULT_SETTINGS: CheckboxSyncPluginSettings = { ignoreSymbols: [], unknownSymbolPolicy: CheckboxState.Checked, enableAutomaticFileSync: false, -}; \ No newline at end of file + consoleEnabled: false, +}; diff --git a/src/ui/CheckboxSyncPluginSettingTab.ts b/src/ui/CheckboxSyncPluginSettingTab.ts index b836b59..801eea2 100644 --- a/src/ui/CheckboxSyncPluginSettingTab.ts +++ b/src/ui/CheckboxSyncPluginSettingTab.ts @@ -15,6 +15,7 @@ import { EnableParentSyncSettingComponent } from "./components/synchronizationBe import { ConfirmModal, InfoModal, SaveConfirmModal } from "./modals"; import { SettingsValidator } from "./validation/SettingsValidator"; import { ValidationError } from "./validation/types"; +import { EnableConsoleLogSettingComponent } from "./components/devGroup/EnableDebug"; export class CheckboxSyncPluginSettingTab extends PluginSettingTab { plugin: CheckboxSyncPlugin; @@ -58,10 +59,18 @@ export class CheckboxSyncPluginSettingTab extends PluginSettingTab { [parentToggleComp, childrenToggleComp, automaticFileSyncToggleComp] ); + const consoleLog = new EnableConsoleLogSettingComponent(); + + const devGroup = new SettingGroup( + "Dev", + [consoleLog] + ); + // Сохраняем созданную группу (или группы) в поле класса this.settingGroups = [ symbolGroup, - behaviorGroup + behaviorGroup, + devGroup ]; const changeListener = () => this.settingChanged(); @@ -294,4 +303,4 @@ export class CheckboxSyncPluginSettingTab extends PluginSettingTab { throw saveError; } } -} \ No newline at end of file +} diff --git a/src/ui/components/devGroup/EnableDebug.ts b/src/ui/components/devGroup/EnableDebug.ts new file mode 100644 index 0000000..c346b98 --- /dev/null +++ b/src/ui/components/devGroup/EnableDebug.ts @@ -0,0 +1,57 @@ +import { ToggleComponent, Setting } from "obsidian"; +import { CheckboxSyncPluginSettings } from "src/types"; +import { BaseSettingComponent } from "src/ui/basedClasses/BaseSettingComponent"; +import { ValidationError } from "src/ui/validation/types"; +import { validateValueIsBoolean } from "src/ui/validation/validators"; + + +export class EnableConsoleLogSettingComponent extends BaseSettingComponent { + private toggleComponent: ToggleComponent; + + constructor() { + super(); + } + + getSettingKey(): keyof CheckboxSyncPluginSettings { + return 'consoleEnabled'; + } + + render(container: HTMLElement, currentValue: any): void { + this.setting = new Setting(container) + .setName('Enable console logs') + .addToggle(toggle => { + this.toggleComponent = toggle; + toggle + .setValue(currentValue as boolean) + .onChange(this.onChangeCallback); + }); + } + + getValueFromUi(): boolean { + if (this.toggleComponent) { + return this.toggleComponent.getValue(); + } + throw new Error(`[${this.getSettingKey()}] Cannot get value from UI before component is rendered.`); + } + + setValueInUi(value: any): void { + if (this.toggleComponent) { + this.toggleComponent.setValue(value as boolean); + } else { + throw new Error(`[${this.getSettingKey()}] Cannot set value before component is rendered.`); + } + } + + validate(): ValidationError | null { + const valueFromUi = this.getValueFromUi(); + const validationResult = validateValueIsBoolean(valueFromUi); + + if (validationResult) { + return { + field: this.getSettingKey(), + message: validationResult.message + }; + } + return null; + } +} diff --git a/src/ui/components/synchronizationBehavior/EnableChildSyncSettingComponent.ts b/src/ui/components/synchronizationBehavior/EnableChildSyncSettingComponent.ts index 1559146..345447a 100644 --- a/src/ui/components/synchronizationBehavior/EnableChildSyncSettingComponent.ts +++ b/src/ui/components/synchronizationBehavior/EnableChildSyncSettingComponent.ts @@ -55,4 +55,4 @@ export class EnableChildSyncSettingComponent extends BaseSettingComponent { } return null; } -} \ No newline at end of file +}