diff --git a/src/EventService.ts b/src/EventService.ts new file mode 100644 index 0000000..e7fe7dd --- /dev/null +++ b/src/EventService.ts @@ -0,0 +1,109 @@ +import { App, MarkdownPostProcessorContext, MarkdownRenderer, MarkdownView, TFile, WorkspaceLeaf } from "obsidian"; +import CheckboxSyncPlugin from "./main"; +import SyncController from "./SyncController"; +import FileStateHolder from "./FileStateHolder"; +import { text } from "stream/consumers"; + +export default class EventService { + + private plugin: CheckboxSyncPlugin; + private app: App; + private syncController: SyncController; + private fileStateHolder: FileStateHolder; + + constructor(plugin: CheckboxSyncPlugin, app: App, syncController: SyncController, fileStateHolder: FileStateHolder) { + this.plugin = plugin; + this.app = app; + this.syncController = syncController; + this.fileStateHolder = fileStateHolder; + + } + + registerEvents() { + //запуск плагина при открытии файла + this.plugin.registerEvent( + this.app.workspace.on("file-open", async (file) => { + if (file) { + console.log(`file-open ${file.path}`); + const isUpdate = await this.fileStateHolder.updateIfNeeded(file); + if (isUpdate) { + await this.syncController.syncFile(file); + } + } + }) + ); + //запуск плагина при модификации файла(для обработки в режиме просмотра) + this.plugin.registerEvent( + this.app.vault.on("modify", async (file) => { + if (!(file instanceof TFile)) return; + if (file.extension !== "md") return; + console.log(`modify file ${file.path}`); + await this.syncController.syncFile(file); + }) + ); + //запуск плагина при изменении в режиме редактора + this.plugin.registerEvent( + this.app.workspace.on("editor-change", async (editor, info) => { + console.log(`editor-change file ${info.file?.path}`); + await this.syncController.syncEditor(editor, info); + }) + ); + + this.plugin.registerMarkdownPostProcessor(async (el: HTMLElement, ctx: MarkdownPostProcessorContext) => { + console.log(`MarkdownPostProcessor "${ctx.sourcePath}"`); + const file = this.app.vault.getAbstractFileByPath(ctx.sourcePath); + if (file instanceof TFile) { + const isUpdate = await this.fileStateHolder.updateIfNeeded(file); + if (isUpdate) { + await this.syncController.syncFile(file); + } + } + }); + + this.plugin.registerEvent( + this.app.workspace.on('quick-preview', async (file: TFile, data: string) => { + const isUpdate = await this.fileStateHolder.updateIfNeeded(file, data); + if (isUpdate) { + await this.syncController.syncFile(file); + } + }) + ); + + this.plugin.app.workspace.onLayoutReady(async () => { + await this.loadAndSyncActiveFiles(); + }); + } + + //загрузка в кеш и синхронизация открытых файлов + async loadAndSyncActiveFiles() { + const markdownLeaves = this.app.workspace.getLeavesOfType('markdown'); + markdownLeaves.forEach(async (leaf: WorkspaceLeaf) => { + await this.loadAndSyncActiveFile(leaf); + }); + } + + private async loadAndSyncActiveFile(leaf: WorkspaceLeaf) { + const view = leaf.view as MarkdownView; + if (!view.file) return; + + //вызов ререндера, для кеширования через MarkdownPostProcessor + await this.rerenderView(view); + } + + // вызывает рендеринг view + private async rerenderView(view: MarkdownView) { + if (!view.file) return; + + if (view.getMode() === 'preview') { + view.previewMode.rerender(); + } else { + await MarkdownRenderer.render( + this.plugin.app, + view.editor.getValue(), + document.createElement('div'), + view.file.path, + this.plugin + ); + } + } +} \ No newline at end of file diff --git a/src/FileStateHolder.ts b/src/FileStateHolder.ts index cc72615..d5abeb2 100644 --- a/src/FileStateHolder.ts +++ b/src/FileStateHolder.ts @@ -1,35 +1,45 @@ -import { TFile } from "obsidian"; -import CheckboxSyncPlugin from "./main"; import { Mutex } from "async-mutex"; +import { TFile, Vault } from "obsidian"; export default class FileStateHolder { private map: Map; - private plugin: CheckboxSyncPlugin; + private vault: Vault; private mutex: Mutex; - constructor(plugin: CheckboxSyncPlugin) { + constructor(vault: Vault) { + this.vault = vault; this.map = new Map(); - this.plugin = plugin; this.mutex = new Mutex(); } async update(file: TFile) { await this.mutex.runExclusive(async () => { - const text = await this.plugin.app.vault.read(file); + const text = await this.vault.read(file); this.set(file, text); }); } - async updateIfNeeded(file: TFile) { - await this.mutex.runExclusive(async () => { - if (!this.map.has(file)) { - const text = await this.plugin.app.vault.read(file); - this.set(file, text); + + // возвращает понадобилась ли загрузка + async updateIfNeeded(file: TFile, text?: string): Promise { + if (this.has(file)) { + return false; + } + let res = await this.mutex.runExclusive(async () => { + if (this.has(file)) { + return false; } + console.log(`updateIfNeeded "${file.name}" start`); + if (!text){ + text = await this.vault.read(file); + } + this.set(file, text); + return true; }); + return res; } - has(file: TFile){ + has(file: TFile) { return this.map.has(file); } diff --git a/src/SyncController.ts b/src/SyncController.ts index 9380774..95bfe22 100644 --- a/src/SyncController.ts +++ b/src/SyncController.ts @@ -1,83 +1,43 @@ import { Mutex } from "async-mutex"; import { Editor, EditorChange, MarkdownFileInfo, MarkdownView, TFile } from "obsidian"; import CheckboxSyncPlugin from "./main"; +import { CheckboxUtils } from "./checkboxUtils"; +import FileStateHolder from "./FileStateHolder"; export default class SyncController { - private plugin: CheckboxSyncPlugin; + private plugin: CheckboxSyncPlugin;//delete + private checkboxUtils: CheckboxUtils; + private fileStateHolder: FileStateHolder; + private mutex: Mutex; - constructor(plugin: CheckboxSyncPlugin) { - this.plugin = plugin; + constructor(plugin: CheckboxSyncPlugin, checkboxUtils: CheckboxUtils, fileStateHolder: FileStateHolder) { + this.plugin = plugin;//delete + this.checkboxUtils = checkboxUtils; + this.fileStateHolder = fileStateHolder; this.mutex = new Mutex(); } async syncEditor(editor: Editor, info: MarkdownView | MarkdownFileInfo) { + if (!info.file) { + return; + } await this.mutex.runExclusive(() => { - console.log(`syncEditor start`); - + const file = info.file!; + console.log(`sync editor "${file.basename}" start.`); const text = editor.getValue(); - if (!info.file) { - console.log(`file not found`); - return; - } - //проверка, что чекбоксы не синхронизированны - const testText = this.plugin.checkboxUtils.syncCheckboxes(text); - if (testText === text) { - console.log(`syncEditor stop, text == testText`); - this.plugin.fileStateHolder.set(info.file, text); - return; - } - const textBefore = this.plugin.fileStateHolder.get(info.file); - let newText = text; + const textBefore = this.fileStateHolder.get(file); - if (textBefore && textBefore!== text) { - const diffs = this.findDifferentLineIndexes(text, textBefore); - for (const diffLineIndex of diffs.reverse()) { - newText = this.plugin.checkboxUtils.syncCheckboxesAfterDifferentLine(newText, diffLineIndex); - } - console.log(`stop diff lines`); - } - - newText = this.plugin.checkboxUtils.syncCheckboxes(newText); + let newText = this.syncText(text, textBefore); if (newText === text) { - this.plugin.fileStateHolder.set(info.file, newText); - console.log(`syncEditor stop, text == newText`); + console.log(`sync editor "${file.basename}" stop. new text equals old text.`); return; } - const cursor = editor.getCursor(); - const newDiffs = this.findDifferentLineIndexes(text, newText); - const lastDifferentLineIndex = newDiffs.length > 0 ? newDiffs[0] : -1; - - const newLines = newText.split("\n"); - const oldLines = text.split("\n"); - if (newLines.length !== oldLines.length) { - throw new Error(); - } - const changes: EditorChange[] = []; - for (let i = 0; i < oldLines.length; i++) { - if (newLines[i] !== oldLines[i]) { - // editor.setLine(i, newLines[i]); - changes.push({ - from: { line: i, ch: 0 }, - to: { line: i, ch: oldLines[i].length }, - text: newLines[i] - }); - } - } - editor.transaction({ - changes: changes - }); - - editor.setCursor(cursor); - if (lastDifferentLineIndex != -1) { - editor.scrollIntoView({ - from: { line: lastDifferentLineIndex, ch: 0 }, - to: { line: lastDifferentLineIndex, ch: 0 } - }); - } - this.plugin.fileStateHolder.set(info.file, newText); - console.log(`syncEditor stop`); + this.editEditor(editor, text, newText); + this.fileStateHolder.set(file, newText); + + console.log(`syncEditor "${file.basename}" stop.`); }); } @@ -86,52 +46,88 @@ export default class SyncController { return; } await this.mutex.runExclusive(async () => { - console.log(`sync file ${file.basename} date: ${Date.now()}`) + console.log(`sync file "${file.basename}" start.`); let text = await this.plugin.app.vault.read(file); - let textBefore = this.plugin.fileStateHolder.get(file); - let newText = text; - if (textBefore) { - const diffs = this.findDifferentLineIndexes(textBefore, text); - for (const diffLineIndex of diffs.reverse()) { - newText = this.plugin.checkboxUtils.syncCheckboxesAfterDifferentLine(newText, diffLineIndex); - } - } + let textBefore = this.fileStateHolder.get(file); + + let newText = this.syncText(text, textBefore); - newText = this.plugin.checkboxUtils.syncCheckboxes(newText); if (newText === text) { + console.log(`sync file "${file.basename}" stop. new text equals old text.`); return; } - await this.plugin.fileStateHolder.set(file, newText); + this.fileStateHolder.set(file, newText); await this.plugin.app.vault.modify(file, newText); + console.log(`sync file "${file.basename}" stop.`); }); } - private findDifferentLineIndexes(text1: string, text2: string): number[] { - const lines1 = text1.split('\n'); - const lines2 = text2.split('\n'); - const minLength = Math.min(lines1.length, lines2.length); + syncText(text: string, textBefore: string | undefined): string { + let newText; + newText = this.syncTextBefore(text, textBefore); + newText = this.checkboxUtils.syncCheckboxes(newText); + return newText; + } + syncTextBefore(text: string, textBefore: string | undefined): string { + if (!textBefore) return text; + const beforeLines = textBefore.split('\n'); + const textLines = text.split('\n'); + + if (beforeLines.length !== textLines.length) return text; + + const diffs = this.findDifferentLineIndexes(beforeLines, textLines); + if (diffs.length !== 1) { + return text; + } + return this.checkboxUtils.syncCheckboxesAfterDifferentLine(text, diffs[0]); + } + + findDifferentLineIndexes(lines1: string[], lines2: string[]): number[] { + if (lines1.length !== lines2.length) { + throw new Error("the length of the lines must be equal"); + } + + const length = lines1.length; const result: number[] = []; - - for (let i = 0; i < minLength; i++) { + for (let i = 0; i < length; i++) { if (lines1[i] !== lines2[i]) { result.push(i); } } - - if (lines1.length !== lines2.length) { - for (let i = Math.min(lines1.length, lines2.length) + 1; i < Math.max(lines1.length, lines2.length); i++) { - result.push(i); - } - } - return result; } - flushChangeIfNeeded(editor: Editor, info: MarkdownView | MarkdownFileInfo) { - if (info instanceof MarkdownView) { - console.log(`MarkdownView ${info.file?.path} flush`); - info.save(); - } + editEditor(editor: Editor, oldText: string, newText: string){ + const cursor = editor.getCursor(); + + const newLines = newText.split("\n"); + const oldLines = oldText.split("\n"); + + const diffIndexes = this.findDifferentLineIndexes(oldLines, newLines); + + const changes: EditorChange[] = []; + + for (let ind of diffIndexes) { + changes.push({ + from: { line: ind, ch: 0 }, + to: { line: ind, ch: oldLines[ind].length }, + text: newLines[ind] + }); + } + editor.transaction({ + changes: changes + }); + + editor.setCursor(cursor); + + const lastDifferentLineIndex = diffIndexes.length > 0 ? diffIndexes[0] : -1; + if (lastDifferentLineIndex != -1) { + editor.scrollIntoView({ + from: { line: lastDifferentLineIndex, ch: 0 }, + to: { line: lastDifferentLineIndex, ch: 0 } + }); + } } + } \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 48090e6..ddcc82a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,9 +1,10 @@ -import { MarkdownPostProcessorContext, MarkdownRenderer, MarkdownView, Plugin, TFile, WorkspaceLeaf } from "obsidian"; -import { CheckboxUtils } from "./checkboxUtils"; +import { Plugin } from "obsidian"; import { CheckboxSyncPluginSettingTab } from "./CheckboxSyncPluginSettingTab"; -import { CheckboxSyncPluginSettings } from "./types"; -import SyncController from "./SyncController"; +import { CheckboxUtils } from "./checkboxUtils"; +import EventService from "./EventService"; import FileStateHolder from "./FileStateHolder"; +import SyncController from "./SyncController"; +import { CheckboxSyncPluginSettings } from "./types"; const DEFAULT_SETTINGS: CheckboxSyncPluginSettings = { xOnlyMode: true, @@ -15,62 +16,18 @@ export default class CheckboxSyncPlugin extends Plugin { syncController: SyncController; checkboxUtils: CheckboxUtils; fileStateHolder: FileStateHolder; + eventService: EventService; async onload() { await this.loadSettings(); - this.fileStateHolder = new FileStateHolder(this); + this.fileStateHolder = new FileStateHolder(this.app.vault); this.checkboxUtils = new CheckboxUtils(this.settings); - this.syncController = new SyncController(this); + this.syncController = new SyncController(this, this.checkboxUtils, this.fileStateHolder); + this.eventService = new EventService(this, this.app, this.syncController, this.fileStateHolder); this.addSettingTab(new CheckboxSyncPluginSettingTab(this.app, this)); - - //запуск плагина при открытии файла - this.registerEvent( - this.app.workspace.on("file-open", async (file) => { - if (file) { - console.log(`file-open ${file.path}`); - await this.fileStateHolder.update(file); - await this.syncController.syncFile(file); - } - }) - ); - //запуск плагина при модификации файла(для обработки в режиме просмотра) - this.registerEvent( - this.app.vault.on("modify", async (file) => { - if (!(file instanceof TFile)) return; - if (file.extension !== "md") return; - - console.log(`modify ${file.path}, extention ${file.extension}`); - await this.syncController.syncFile(file); - }) - ); - //запуск плагина при изменении в режиме редактора - this.registerEvent( - this.app.workspace.on("editor-change", async (editor, info) => { - console.log(`editor-change ${info.file?.path}`); - await this.syncController.syncEditor(editor, info); - }) - ); - - this.registerMarkdownPostProcessor(async (el: HTMLElement, ctx: MarkdownPostProcessorContext) => { - console.log(`MarkdownPostProcessor ${ctx.sourcePath}`); - const file = this.app.vault.getAbstractFileByPath(ctx.sourcePath); - if (file instanceof TFile) { - await this.fileStateHolder.updateIfNeeded(file); - } - }); - this.registerEvent( - this.app.workspace.on('quick-preview', async (file: TFile, data: string) => { - if (!this.fileStateHolder.has(file)) { - this.fileStateHolder.set(file, data); - } - }) - ); - - this.app.workspace.onLayoutReady(async () => { - await this.updateActiveFiles(); - }); + this.eventService.registerEvents(); } @@ -81,31 +38,6 @@ export default class CheckboxSyncPlugin extends Plugin { async saveSettings() { await this.saveData(this.settings); this.checkboxUtils.updateSettings(this.settings); - await this.updateActiveFiles(); + await this.eventService.loadAndSyncActiveFiles(); } - - async updateActiveFiles() { - const markdownLeaves = this.app.workspace.getLeavesOfType('markdown'); - markdownLeaves.forEach(async (leaf: WorkspaceLeaf) => { - const view = leaf.view as MarkdownView; - if (!view.file) return; - console.log(`${view.file?.basename} mod ${view.getMode()}`) - if (view.getMode() === 'preview') { - console.log(`rerender ${view.file.basename}`); - view.previewMode.rerender(); - await this.fileStateHolder.updateIfNeeded(view.file); - } else { - const content = view.editor.getValue(); - const tempEl = document.createElement('div'); - await MarkdownRenderer.render( - this.app, - content, - tempEl, - view.file!.path, - this - ); - } - await this.syncController.syncFile(view.file); - }); - } -} \ No newline at end of file +} diff --git a/src/utils/MultiMap.ts b/src/utils/MultiMap.ts deleted file mode 100644 index 4adc4f4..0000000 --- a/src/utils/MultiMap.ts +++ /dev/null @@ -1,67 +0,0 @@ -export default class MultiMap { - private map: Map; - - constructor() { - this.map = new Map(); - } - - // Добавление значения к ключу - add(key: K, value: V): void { - if (!this.map.has(key)) { - this.map.set(key, []); - } - this.map.get(key)?.push(value); - } - - // Получение всех значений для ключа - get(key: K): V[] | undefined { - return this.map.get(key); - } - - // Удаление конкретного значения у ключа - remove(key: K, value: V): boolean { - const values = this.map.get(key); - if (!values) return false; - - const index = values.indexOf(value); - if (index === -1) return false; - - values.splice(index, 1); - - if (values.length === 0) { - this.map.delete(key); - } - - return true; - } - - // Удаление всех значений у ключа - delete(key: K): boolean { - return this.map.delete(key); - } - - // Проверка существования ключа - has(key: K): boolean { - return this.map.has(key); - } - - // Очистка всей карты - clear(): void { - this.map.clear(); - } - - // Итерация по всем парам ключ-значения - entries(): IterableIterator<[K, V[]]> { - return this.map.entries(); - } - - // Получение всех ключей - keys(): IterableIterator { - return this.map.keys(); - } - - // Получение всех значений - values(): IterableIterator { - return this.map.values(); - } -}