mirror of
https://github.com/groldsf/obsidian_check_plugin.git
synced 2026-07-22 12:00:31 +00:00
stable ver? bugs...
This commit is contained in:
parent
d3e7b14593
commit
32a5fe4229
5 changed files with 230 additions and 250 deletions
109
src/EventService.ts
Normal file
109
src/EventService.ts
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
92
src/main.ts
92
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue