Add local prediction for remote cursor updates

This commit is contained in:
Andras Schmelczer 2025-08-17 15:03:34 +01:00
parent b7e80c39f1
commit e73f147fbc
No known key found for this signature in database
GPG key ID: FC8F2C3D3D1A718C
6 changed files with 207 additions and 40 deletions

View file

@ -11,7 +11,7 @@ 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 { SyncClient, rateLimit, DEFAULT_SETTINGS, Logger } from "sync-client";
import { ObsidianFileSystemOperations } from "./obsidian-file-system";
import { SyncSettingsTab } from "./views/settings/settings-tab";
import { logToConsole } from "./utils/log-to-console";
@ -26,6 +26,7 @@ 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)[] = [];
@ -61,7 +62,8 @@ export default class VaultLinkPlugin extends Plugin {
load: this.loadData.bind(this),
save: this.saveData.bind(this)
},
nativeLineEndings: Platform.isWin ? "\r\n" : "\n"
nativeLineEndings: Platform.isWin ? "\r\n" : "\n",
...debugOptions
});
logToConsole(this.client);

View file

@ -7,7 +7,6 @@ import { getSelectionsFromEditor } from "./get-selections-from-editor";
export class LocalCursorUpdateListener {
private static readonly UPDATE_INTERVAL_MS = 50;
private readonly eventHandle: NodeJS.Timeout;
private lastCursorState: Record<string, Selection[]> = {};
public constructor(
private readonly client: SyncClient,
@ -24,13 +23,6 @@ export class LocalCursorUpdateListener {
private updateAllSelections(): void {
const currentCursors = this.getAllSelections();
if (
JSON.stringify(this.lastCursorState) ===
JSON.stringify(currentCursors)
) {
return;
}
this.lastCursorState = currentCursors;
this.client
.updateLocalCursors(currentCursors)
.catch((error: unknown) => {

View file

@ -1,6 +1,6 @@
import type { Range } from "@codemirror/state";
import { RangeSet, Annotation, AnnotationType } from "@codemirror/state";
import { ViewPlugin, Decoration, WidgetType } from "@codemirror/view";
import { RangeSet } from "@codemirror/state";
import { ViewPlugin, Decoration } from "@codemirror/view";
import type {
PluginValue,
@ -9,7 +9,10 @@ import type {
ViewUpdate
} from "@codemirror/view";
import { RemoteCursorWidget } from "./remote-cursor-widget";
import type { ClientCursors, CursorSpan } from "sync-client";
import type {
CursorSpan,
DocumentWithMaybeOutdatedClientCursors
} from "sync-client";
import type { App } from "obsidian";
import { MarkdownView } from "obsidian";
@ -17,10 +20,12 @@ let cursors: {
name: string;
path: string;
span: CursorSpan;
deviceId: string;
}[] = [];
import { StateEffect } from "@codemirror/state";
import { getRandomColor } from "src/utils/get-random-color";
import { updateSelection } from "./update-selection";
const forceUpdate = StateEffect.define();
@ -28,6 +33,17 @@ export class RemoteCursorsPluginValue implements PluginValue {
public decorations: DecorationSet = RangeSet.of([]);
public update(update: ViewUpdate): void {
update.changes.iterChanges((fromA, toA, fromB, toB, _inserted) => {
const spans = cursors.map((cursor) => cursor.span);
updateSelection({
fromA,
toA,
fromB,
toB,
spans
});
});
const decorations: Range<Decoration>[] = [];
cursors.forEach(({ name, span: { start, end } }) => {
@ -103,20 +119,30 @@ export const remoteCursorsPlugin = ViewPlugin.fromClass(
}
);
export function setCursors(clients: ClientCursors[], app: App): void {
cursors = clients.flatMap((client) => {
const clientCursors = client.cursors;
return Object.keys(clientCursors).flatMap((path) => {
const spans = clientCursors[path];
return spans
? spans.map((span) => ({
export function setCursors(
clients: DocumentWithMaybeOutdatedClientCursors[],
app: App
): void {
cursors = [
...cursors.filter(({ deviceId }) =>
clients.some(
(client) => client.deviceId === deviceId && client.isOutdated
)
),
...clients
.filter(({ isOutdated }) => !isOutdated)
.flatMap((client) => {
const clientCursors = client.documentsWithCursors;
return clientCursors.flatMap((cursor) =>
cursor.cursors.map((span) => ({
name: client.userName,
path,
path: cursor.relative_path,
deviceId: client.deviceId,
span
}))
: [];
});
});
);
})
];
app.workspace
.getLeavesOfType("markdown")