227 lines
5.8 KiB
TypeScript
227 lines
5.8 KiB
TypeScript
import type {
|
|
Editor,
|
|
MarkdownFileInfo,
|
|
TAbstractFile,
|
|
WorkspaceLeaf
|
|
} from "obsidian";
|
|
import type { MarkdownView } from "obsidian";
|
|
import { Platform, Plugin, TFile } from "obsidian";
|
|
import "../manifest.json";
|
|
import { HistoryView } from "./views/history/history-view";
|
|
import { StatusBar } from "./views/status-bar/status-bar";
|
|
import { LogsView } from "./views/logs/logs-view";
|
|
import { StatusDescription } from "./views/status-description/status-description";
|
|
import { SyncClient, rateLimit, DEFAULT_SETTINGS } from "sync-client";
|
|
import { ObsidianFileSystemOperations } from "./obsidian-file-system";
|
|
import { SyncSettingsTab } from "./views/settings/settings-tab";
|
|
import { logToConsole } from "./utils/log-to-console";
|
|
import { updateEditorStatusDisplay } from "./views/editor-sync-line/editor-sync-line";
|
|
import { remoteCursorsTheme } from "./views/cursors/remote-cursor-theme";
|
|
import {
|
|
remoteCursorsPlugin,
|
|
setCursors
|
|
} from "./views/cursors/remote-cursors-plugin";
|
|
import { LocalCursorUpdateListener } from "./views/cursors/local-cursor-update-listener";
|
|
import { slowFetchFactory } from "./debugging/slow-fetch-factory";
|
|
import { flakyWebSocketFactory } from "./debugging/flaky-websocket-factory";
|
|
|
|
const MIN_WAIT_BETWEEN_UPDATES_IN_MS = 250;
|
|
export default class VaultLinkPlugin extends Plugin {
|
|
private readonly disposables: (() => unknown)[] = [];
|
|
|
|
private settingsTab: SyncSettingsTab | undefined;
|
|
private client!: SyncClient;
|
|
private readonly rateLimitedUpdatesPerFile = new Map<
|
|
string,
|
|
() => Promise<unknown>
|
|
>();
|
|
|
|
public async onload(): Promise<void> {
|
|
DEFAULT_SETTINGS.ignorePatterns.push(
|
|
".obsidian/**",
|
|
".git/**",
|
|
".trash/**"
|
|
);
|
|
|
|
const isDebugBuild = process.env.NODE_ENV === "development";
|
|
|
|
const debugOptions = isDebugBuild
|
|
? {
|
|
fetch: slowFetchFactory(1),
|
|
webSocket: flakyWebSocketFactory(1, new Logger())
|
|
}
|
|
: {};
|
|
|
|
this.client = await SyncClient.create({
|
|
fs: new ObsidianFileSystemOperations(
|
|
this.app.vault,
|
|
this.app.workspace
|
|
),
|
|
persistence: {
|
|
load: this.loadData.bind(this),
|
|
save: this.saveData.bind(this)
|
|
},
|
|
nativeLineEndings: Platform.isWin ? "\r\n" : "\n"
|
|
});
|
|
|
|
logToConsole(this.client);
|
|
|
|
const statusDescription = new StatusDescription(this.client);
|
|
|
|
this.settingsTab = new SyncSettingsTab({
|
|
app: this.app,
|
|
plugin: this,
|
|
syncClient: this.client,
|
|
statusDescription
|
|
});
|
|
this.addSettingTab(this.settingsTab);
|
|
|
|
new StatusBar(this, this.client);
|
|
|
|
this.registerView(
|
|
HistoryView.TYPE,
|
|
(leaf) => new HistoryView(this.client, leaf)
|
|
);
|
|
|
|
this.registerView(
|
|
LogsView.TYPE,
|
|
(leaf) => new LogsView(this.client, leaf)
|
|
);
|
|
|
|
this.registerEditorExtension([remoteCursorsTheme, remoteCursorsPlugin]);
|
|
|
|
this.client.addRemoteCursorsUpdateListener((cursors) => {
|
|
setCursors(cursors, this.app);
|
|
});
|
|
|
|
const cursorListener = new LocalCursorUpdateListener(
|
|
this.client,
|
|
this.app.workspace
|
|
);
|
|
this.disposables.push(() => {
|
|
cursorListener.dispose();
|
|
});
|
|
|
|
this.app.workspace.updateOptions();
|
|
|
|
this.addRibbonIcon(
|
|
HistoryView.ICON,
|
|
"Open VaultLink events",
|
|
async (_: MouseEvent) => this.activateView(HistoryView.TYPE)
|
|
);
|
|
|
|
this.addRibbonIcon(
|
|
LogsView.ICON,
|
|
"Open VaultLink logs",
|
|
async (_: MouseEvent) => this.activateView(LogsView.TYPE)
|
|
);
|
|
|
|
this.app.workspace.onLayoutReady(async () => {
|
|
this.registerEditorEvents();
|
|
void this.client.start();
|
|
|
|
const interval = setInterval(() => {
|
|
updateEditorStatusDisplay(this.app.workspace, this.client);
|
|
}, 200);
|
|
this.disposables.push(() => {
|
|
clearInterval(interval);
|
|
});
|
|
});
|
|
}
|
|
|
|
public onunload(): void {
|
|
this.client.stop();
|
|
this.disposables.forEach((disposable) => {
|
|
disposable();
|
|
});
|
|
}
|
|
|
|
public openSettings(): void {
|
|
// eslint-disable-next-line
|
|
(this.app as any).setting.open(); // this is undocumented
|
|
// eslint-disable-next-line
|
|
(this.app as any).setting.openTab(this.settingsTab); // this is undocumented
|
|
}
|
|
|
|
public closeSettings(): void {
|
|
// eslint-disable-next-line
|
|
(this.app as any).setting.close(); // this is undocumented
|
|
}
|
|
|
|
public async activateView(type: string): Promise<void> {
|
|
const { workspace } = this.app;
|
|
|
|
let leaf: WorkspaceLeaf | null = null;
|
|
const leaves = workspace.getLeavesOfType(type);
|
|
|
|
if (leaves.length > 0) {
|
|
[leaf] = leaves;
|
|
} else {
|
|
leaf = workspace.getRightLeaf(false);
|
|
await leaf?.setViewState({ type: type, active: true });
|
|
}
|
|
|
|
if (leaf) {
|
|
await workspace.revealLeaf(leaf);
|
|
}
|
|
}
|
|
|
|
private registerEditorEvents(): void {
|
|
[
|
|
this.app.workspace.on(
|
|
"editor-change",
|
|
async (
|
|
_editor: Editor,
|
|
info: MarkdownView | MarkdownFileInfo
|
|
) => {
|
|
const { file } = info;
|
|
if (file) {
|
|
await this.rateLimitedUpdate(file.path);
|
|
}
|
|
}
|
|
),
|
|
this.app.vault.on("create", async (file: TAbstractFile) => {
|
|
if (file instanceof TFile) {
|
|
await this.client.syncLocallyCreatedFile(file.path);
|
|
}
|
|
}),
|
|
this.app.vault.on("modify", async (file: TAbstractFile) => {
|
|
if (file instanceof TFile) {
|
|
await this.rateLimitedUpdate(file.path);
|
|
}
|
|
}),
|
|
this.app.vault.on("delete", async (file: TAbstractFile) => {
|
|
await this.client.syncLocallyDeletedFile(file.path);
|
|
}),
|
|
this.app.vault.on(
|
|
"rename",
|
|
async (file: TAbstractFile, oldPath: string) => {
|
|
if (file instanceof TFile) {
|
|
await this.client.syncLocallyUpdatedFile({
|
|
oldPath,
|
|
relativePath: file.path
|
|
});
|
|
}
|
|
}
|
|
)
|
|
].forEach((event) => {
|
|
this.registerEvent(event);
|
|
});
|
|
}
|
|
|
|
private async rateLimitedUpdate(path: string): Promise<void> {
|
|
if (!this.rateLimitedUpdatesPerFile.has(path)) {
|
|
this.rateLimitedUpdatesPerFile.set(
|
|
path,
|
|
rateLimit(
|
|
async () =>
|
|
this.client.syncLocallyUpdatedFile({
|
|
relativePath: path
|
|
}),
|
|
MIN_WAIT_BETWEEN_UPDATES_IN_MS
|
|
)
|
|
);
|
|
}
|
|
await this.rateLimitedUpdatesPerFile.get(path)?.();
|
|
}
|
|
}
|