stable ver? bugs...

This commit is contained in:
Grol Grol 2025-04-06 21:58:56 +03:00
parent d3e7b14593
commit 32a5fe4229
5 changed files with 230 additions and 250 deletions

109
src/EventService.ts Normal file
View file

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

View file

@ -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<TFile, string>;
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<boolean> {
if (this.has(file)) {
return false;
}
let res = await this.mutex.runExclusive<boolean>(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);
}

View file

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

View file

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

View file

@ -1,67 +0,0 @@
export default class MultiMap<K, V> {
private map: Map<K, V[]>;
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<K> {
return this.map.keys();
}
// Получение всех значений
values(): IterableIterator<V[]> {
return this.map.values();
}
}