Add API for propagating cursor locations #61

Merged
schmelczer merged 30 commits from asch/show-cursors into main 2025-06-08 20:20:53 +01:00
5 changed files with 87 additions and 9 deletions
Showing only changes of commit f4c77ddd25 - Show all commits

Send cursors instantly

Andras Schmelczer 2025-06-08 11:32:41 +01:00
No known key found for this signature in database
GPG key ID: FC8F2C3D3D1A718C

View file

@ -48,6 +48,10 @@ impl Cursors {
device_id: device_id.to_string(), device_id: device_id.to_string(),
cursors: document_to_cursors, 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<ClientCursors> { pub async fn get_cursors(&self, vault_id: &VaultId) -> Vec<ClientCursors> {
@ -73,7 +77,6 @@ impl Cursors {
async fn run_backround_task(&self) { async fn run_backround_task(&self) {
loop { loop {
self.remove_expired_cursors().await; self.remove_expired_cursors().await;
self.broadcast_cursors().await;
tokio::time::sleep(self.config.cursor_broadcast_interval).await; tokio::time::sleep(self.config.cursor_broadcast_interval).await;
} }
} }

View file

@ -7,7 +7,7 @@ import type {
} from "sync-client"; } from "sync-client";
import { lineAndColumnToPosition } from "./utils/line-and-column-to-position"; import { lineAndColumnToPosition } from "./utils/line-and-column-to-position";
import { positionToLineAndColumn } from "./utils/position-to-line-and-column"; 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 { export class ObsidianFileSystemOperations implements FileSystemOperations {
public constructor( public constructor(

View file

@ -1,27 +1,36 @@
import type { import type {
Editor, Editor,
EventRef,
MarkdownFileInfo, MarkdownFileInfo,
MarkdownView,
TAbstractFile, TAbstractFile,
Workspace,
WorkspaceLeaf WorkspaceLeaf
} from "obsidian"; } from "obsidian";
import { MarkdownView } from "obsidian";
import { Platform, Plugin, TFile } from "obsidian"; import { Platform, Plugin, TFile } from "obsidian";
import "../manifest.json"; import "../manifest.json";
import { HistoryView } from "./views/history/history-view"; import { HistoryView } from "./views/history/history-view";
import { StatusBar } from "./views/status-bar/status-bar"; import { StatusBar } from "./views/status-bar/status-bar";
import { LogsView } from "./views/logs/logs-view"; import { LogsView } from "./views/logs/logs-view";
import { StatusDescription } from "./views/status-description/status-description"; import { StatusDescription } from "./views/status-description/status-description";
import type { CursorSpan, RelativePath } from "sync-client";
import { SyncClient, rateLimit, DEFAULT_SETTINGS } from "sync-client"; import { SyncClient, rateLimit, DEFAULT_SETTINGS } from "sync-client";
import { ObsidianFileSystemOperations } from "./obsidian-file-system"; import { ObsidianFileSystemOperations } from "./obsidian-file-system";
import { SyncSettingsTab } from "./views/settings/settings-tab"; import { SyncSettingsTab } from "./views/settings/settings-tab";
import { registerConsoleForLogging } from "./utils/register-console-for-logging"; import { registerConsoleForLogging } from "./utils/register-console-for-logging";
import { updateEditorStatusDisplay } from "./views/editor-sync-line/editor-sync-line"; import { updateEditorStatusDisplay } from "./views/editor-sync-line/editor-sync-line";
import { remoteCursorsTheme } from "./views/remote-cursors/remote-cursor-theme"; import { remoteCursorsTheme } from "./views/cursors/remote-cursor-theme";
import { remoteCursorsPlugin } from "./views/remote-cursors/remote-cursors-plugin"; 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; const MIN_WAIT_BETWEEN_UPDATES_IN_MS = 250;
export default class VaultLinkPlugin extends Plugin { export default class VaultLinkPlugin extends Plugin {
private readonly disposables: (() => unknown)[] = []; private readonly disposables: (() => unknown)[] = [];
private settingsTab: SyncSettingsTab | undefined; private settingsTab: SyncSettingsTab | undefined;
private client!: SyncClient; private client!: SyncClient;
private readonly rateLimitedUpdatesPerFile = new Map< private readonly rateLimitedUpdatesPerFile = new Map<
@ -73,6 +82,17 @@ export default class VaultLinkPlugin extends Plugin {
); );
this.registerEditorExtension([remoteCursorsTheme, remoteCursorsPlugin]); 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.app.workspace.updateOptions();
this.addRibbonIcon( this.addRibbonIcon(
@ -175,9 +195,7 @@ export default class VaultLinkPlugin extends Plugin {
} }
} }
) )
].forEach((event) => { ].forEach((event) => this.registerEvent(event));
this.registerEvent(event);
});
} }
private async rateLimitedUpdate(path: string): Promise<void> { private async rateLimitedUpdate(path: string): Promise<void> {

View file

@ -1,5 +1,5 @@
import type { Editor } from "obsidian"; 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 { export interface Cursor {
id: number; id: number;

View file

@ -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<string, Cursor[]> = {};
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<string, Cursor[]> {
const cursors: Record<string, Cursor[]> = {};
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);
}
}