feat(settings): Add option to enable/disable debug logging

This commit is contained in:
Grol Grol 2025-05-14 13:24:08 +03:00
parent 28ebd1ddf0
commit 595bb8194e
9 changed files with 232 additions and 50 deletions

View file

@ -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,6 +112,13 @@ These settings control when and how the synchronization logic runs.
- **Включено:** Автоматически синхронизирует состояния чекбоксов при загрузке/открытии файлов и сразу после применения настроек плагина. Это обеспечивает консистентность, но может влиять на производительность в очень больших хранилищах или файлах. *(Требует перезапуска Obsidian или перезагрузки настроек для полного вступления в силу)*.
- **Отключено (По умолчанию):** Синхронизация происходит только тогда, когда вы *вручную* изменяете состояние чекбокса в Obsidian. Это поведение по умолчанию для минимизации потенциального влияния на производительность.
### Разработка / Отладка
- **Включить Отладочное Логирование (Enable console log)**
- **Описание:** Переключает вывод подробных логов в консоль разработчика. Когда включено, плагин будет выводить больше информации о своих операциях, что может быть полезно для устранения неполадок или понимания его поведения.
- **Включено:** Отладочные логи выводятся в консоль.
- **Отключено (По умолчанию):** Отладочные логи не выводятся.
### Действия и Статус
- **Отображение Ошибок:** Область под настройками отображает любые возникшие ошибки, например, невалидный JSON в конфигурации символов.

View file

@ -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;
}
});
},
});

View file

@ -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) {

2
package-lock.json generated
View file

@ -7,7 +7,7 @@
"": {
"name": "checkbox-sync",
"version": "1.0.0",
"license": "MIT",
"license": "0BSD",
"dependencies": {
"async-mutex": "^0.5.0"
},

View file

@ -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<CheckboxSyncPluginSettings> {
return this._settings;
}
async updateSettings(callback: (settings: CheckboxSyncPluginSettings) => void | Promise<void>) {
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<CheckboxSyncPluginSettings> {
return this._settings;
}
async updateSettings(callback: (settings: CheckboxSyncPluginSettings) => void | Promise<void>) {
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;
}
}

View file

@ -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,
consoleEnabled: false,
};

View file

@ -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();

View file

@ -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;
}
}