From f4c77ddd25b40e48e1ba9e534a4c836d861acc48 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 8 Jun 2025 11:32:41 +0100 Subject: [PATCH] Send cursors instantly --- backend/sync_server/src/app_state/cursors.rs | 5 +- .../src/obsidian-file-system.ts | 2 +- .../obsidian-plugin/src/vault-link-plugin.ts | 30 ++++++++-- .../cursors}/get-cursors-from-editor.ts | 2 +- .../cursors/local-cursor-update-listener.ts | 57 +++++++++++++++++++ 5 files changed, 87 insertions(+), 9 deletions(-) rename frontend/obsidian-plugin/src/{utils => views/cursors}/get-cursors-from-editor.ts (83%) create mode 100644 frontend/obsidian-plugin/src/views/cursors/local-cursor-update-listener.ts diff --git a/backend/sync_server/src/app_state/cursors.rs b/backend/sync_server/src/app_state/cursors.rs index a2dc6807..6b8a8605 100644 --- a/backend/sync_server/src/app_state/cursors.rs +++ b/backend/sync_server/src/app_state/cursors.rs @@ -48,6 +48,10 @@ impl Cursors { device_id: device_id.to_string(), cursors: document_to_cursors, })); + + drop(vault_to_cursors); // Explicitly drop the lock before broadcasting to avoid deadlock + + self.broadcast_cursors().await; } pub async fn get_cursors(&self, vault_id: &VaultId) -> Vec { @@ -73,7 +77,6 @@ impl Cursors { async fn run_backround_task(&self) { loop { self.remove_expired_cursors().await; - self.broadcast_cursors().await; tokio::time::sleep(self.config.cursor_broadcast_interval).await; } } diff --git a/frontend/obsidian-plugin/src/obsidian-file-system.ts b/frontend/obsidian-plugin/src/obsidian-file-system.ts index 6546b0fb..adf78a16 100644 --- a/frontend/obsidian-plugin/src/obsidian-file-system.ts +++ b/frontend/obsidian-plugin/src/obsidian-file-system.ts @@ -7,7 +7,7 @@ import type { } from "sync-client"; import { lineAndColumnToPosition } from "./utils/line-and-column-to-position"; import { positionToLineAndColumn } from "./utils/position-to-line-and-column"; -import { getCursorsFromEditor } from "./utils/get-cursors-from-editor"; +import { getCursorsFromEditor } from "./views/cursors/get-cursors-from-editor"; export class ObsidianFileSystemOperations implements FileSystemOperations { public constructor( diff --git a/frontend/obsidian-plugin/src/vault-link-plugin.ts b/frontend/obsidian-plugin/src/vault-link-plugin.ts index efd54417..9b9d62ab 100644 --- a/frontend/obsidian-plugin/src/vault-link-plugin.ts +++ b/frontend/obsidian-plugin/src/vault-link-plugin.ts @@ -1,27 +1,36 @@ import type { Editor, + EventRef, MarkdownFileInfo, - MarkdownView, TAbstractFile, + Workspace, WorkspaceLeaf } from "obsidian"; +import { 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 type { CursorSpan, RelativePath } from "sync-client"; import { SyncClient, rateLimit, DEFAULT_SETTINGS } from "sync-client"; import { ObsidianFileSystemOperations } from "./obsidian-file-system"; import { SyncSettingsTab } from "./views/settings/settings-tab"; import { registerConsoleForLogging } from "./utils/register-console-for-logging"; import { updateEditorStatusDisplay } from "./views/editor-sync-line/editor-sync-line"; -import { remoteCursorsTheme } from "./views/remote-cursors/remote-cursor-theme"; -import { remoteCursorsPlugin } from "./views/remote-cursors/remote-cursors-plugin"; +import { remoteCursorsTheme } from "./views/cursors/remote-cursor-theme"; +import { + remoteCursorsPlugin, + setCursors +} from "./views/cursors/remote-cursors-plugin"; +import { getCursorsFromEditor } from "./views/cursors/get-cursors-from-editor"; +import { LocalCursorUpdateListener } from "./views/cursors/local-cursor-update-listener"; 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< @@ -73,6 +82,17 @@ export default class VaultLinkPlugin extends Plugin { ); 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( @@ -175,9 +195,7 @@ export default class VaultLinkPlugin extends Plugin { } } ) - ].forEach((event) => { - this.registerEvent(event); - }); + ].forEach((event) => this.registerEvent(event)); } private async rateLimitedUpdate(path: string): Promise { diff --git a/frontend/obsidian-plugin/src/utils/get-cursors-from-editor.ts b/frontend/obsidian-plugin/src/views/cursors/get-cursors-from-editor.ts similarity index 83% rename from frontend/obsidian-plugin/src/utils/get-cursors-from-editor.ts rename to frontend/obsidian-plugin/src/views/cursors/get-cursors-from-editor.ts index 62113d1b..f5ea0a85 100644 --- a/frontend/obsidian-plugin/src/utils/get-cursors-from-editor.ts +++ b/frontend/obsidian-plugin/src/views/cursors/get-cursors-from-editor.ts @@ -1,5 +1,5 @@ import type { Editor } from "obsidian"; -import { lineAndColumnToPosition } from "./line-and-column-to-position"; +import { lineAndColumnToPosition } from "../../utils/line-and-column-to-position"; export interface Cursor { id: number; diff --git a/frontend/obsidian-plugin/src/views/cursors/local-cursor-update-listener.ts b/frontend/obsidian-plugin/src/views/cursors/local-cursor-update-listener.ts new file mode 100644 index 00000000..319ae285 --- /dev/null +++ b/frontend/obsidian-plugin/src/views/cursors/local-cursor-update-listener.ts @@ -0,0 +1,57 @@ +import { + EventRef, + Workspace, + Editor, + MarkdownView, + MarkdownFileInfo +} from "obsidian"; +import { SyncClient } from "sync-client"; +import { Cursor, getCursorsFromEditor } from "./get-cursors-from-editor"; + +export class LocalCursorUpdateListener { + private static readonly UPDATE_INTERVAL_MS = 50; + private readonly eventHandle: NodeJS.Timeout; + private lastCursorState: Record = {}; + + public constructor( + private readonly client: SyncClient, + private readonly workspace: Workspace + ) { + this.eventHandle = setInterval( + () => this.updateAllCursors(), + LocalCursorUpdateListener.UPDATE_INTERVAL_MS + ); + } + + private updateAllCursors(): void { + const currentCursors = this.getAllCursors(); + if ( + JSON.stringify(this.lastCursorState) === + JSON.stringify(currentCursors) + ) { + return; + } + this.lastCursorState = currentCursors; + this.client.updateLocalCursors(currentCursors); + } + + private getAllCursors(): Record { + const cursors: Record = {}; + this.workspace + .getLeavesOfType("markdown") + .map((leaf) => leaf.view) + .filter((view) => view instanceof MarkdownView) + .forEach((view) => { + const { file } = view; + if (!file) { + return; + } + cursors[file.path] = getCursorsFromEditor(view.editor); + }); + return cursors; + } + + public dispose(): void { + clearInterval(this.eventHandle); + } +}