Skip to content

Commit

Permalink
fix(cursor): optimized cursor synchronization debouncing (#1406)
Browse files Browse the repository at this point in the history
* fix(cursor): optimized cursor synchronization debouncing

* perf: use request

* fix: remove  `maxWait` option

* chore: lint

* chore: make code more readable

* chore: tweaks
  • Loading branch information
xiyaowong committed Sep 1, 2023
1 parent 6d7a4a2 commit 7326aae
Showing 1 changed file with 69 additions and 46 deletions.
115 changes: 69 additions & 46 deletions src/cursor_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,9 @@ import {

import { Logger } from "./logger";
import { MainController } from "./main_controller";
import { NeovimExtensionRequestProcessable, NeovimRedrawProcessable } from "./neovim_events_processable";
import {
callAtomic,
convertEditorPositionToVimPosition,
convertVimPositionToEditorPosition,
ManualPromise,
} from "./utils";
import { Mode } from "./mode_manager";
import { NeovimExtensionRequestProcessable, NeovimRedrawProcessable } from "./neovim_events_processable";
import { convertEditorPositionToVimPosition, convertVimPositionToEditorPosition, ManualPromise } from "./utils";

const LOG_PREFIX = "CursorManager";

Expand Down Expand Up @@ -63,6 +58,12 @@ export class CursorManager implements Disposable, NeovimRedrawProcessable, Neovi
private debouncedCursorUpdates: WeakMap<TextEditor, DebouncedFunc<CursorManager["updateCursorPosInEditor"]>> =
new WeakMap();

// Different change kinds use different debounce times
private debouncedApplySelectionChanged: Map<number, DebouncedFunc<CursorManager["applySelectionChanged"]>> =
new Map();
// A flag indicates that func still pending.
private previousApplyDebounceTime: number | undefined;

public constructor(
private logger: Logger,
private client: NeovimClient,
Expand Down Expand Up @@ -195,17 +196,19 @@ export class CursorManager implements Disposable, NeovimRedrawProcessable, Neovi
if (!modeConf) {
return;
}
let style: TextEditorCursorStyle;
if (modeName == "visual") {
// in visual mode, we try to hide the cursor because we only use it for selections
style = TextEditorCursorStyle.LineThin;
} else if (modeConf.cursorShape === "block") {
style = TextEditorCursorStyle.Block;
} else if (modeConf.cursorShape === "horizontal") {
style = TextEditorCursorStyle.Underline;
} else {
style = TextEditorCursorStyle.Line;
}
for (const editor of window.visibleTextEditors) {
if (modeName == "visual") {
// in visual mode, we try to hide the cursor because we only use it for selections
editor.options.cursorStyle = TextEditorCursorStyle.LineThin;
} else if (modeConf.cursorShape === "block") {
editor.options.cursorStyle = TextEditorCursorStyle.Block;
} else if (modeConf.cursorShape === "horizontal") {
editor.options.cursorStyle = TextEditorCursorStyle.Underline;
} else {
editor.options.cursorStyle = TextEditorCursorStyle.Line;
}
editor.options.cursorStyle = style;
}
}

Expand Down Expand Up @@ -302,34 +305,56 @@ export class CursorManager implements Disposable, NeovimRedrawProcessable, Neovi
this.updateCursorStyle("visual");
}

this.applySelectionChanged(textEditor, kind);
this.getDebouncedApplySelectionChanged(kind)(textEditor, kind);
};

// ! Need to debounce requests because setting cursor by consequence of neovim event will trigger this method
// ! and cursor may go out-of-sync and produce a jitter
private applySelectionChanged = debounce(
async (editor: TextEditor, kind: TextEditorSelectionChangeKind | undefined) => {
// reset cursor style if needed
this.updateCursorStyle(this.main.modeManager.currentMode.name);

// wait for possible layout updates first
this.logger.debug(`${LOG_PREFIX}: Waiting for possible layout completion operation`);
await this.main.bufferManager.waitForLayoutSync();
// wait for possible change document events
this.logger.debug(`${LOG_PREFIX}: Waiting for possible document change completion operation`);
await this.main.changeManager.getDocumentChangeCompletionLock(editor.document);
this.logger.debug(`${LOG_PREFIX}: Waiting done`);

// ignore selection change caused by buffer edit
const selection = editor.selection;
const documentChange = this.main.changeManager.eatDocumentCursorAfterChange(editor.document);
if (documentChange && documentChange.isEqual(selection.active)) {
this.logger.debug(
`${LOG_PREFIX}: Skipping onSelectionChanged event since it was selection produced by doc change`,
);
return;
}
private getDebouncedApplySelectionChanged = (
kind: TextEditorSelectionChangeKind | undefined,
): DebouncedFunc<CursorManager["applySelectionChanged"]> => {
let debounceTime: number;
// Should use same debounce time if previous debounced func still in progress
// This avoid multiple cursor updates with different positions at the same time
if (this.previousApplyDebounceTime !== undefined) {
debounceTime = this.previousApplyDebounceTime;
} else if (kind === TextEditorSelectionChangeKind.Mouse) {
debounceTime = 100;
} else {
debounceTime = 50;
}
this.previousApplyDebounceTime = debounceTime;

let func = this.debouncedApplySelectionChanged.get(debounceTime);
if (func) return func;
func = debounce(this.applySelectionChanged, debounceTime, { leading: false, trailing: true });
this.debouncedApplySelectionChanged.set(debounceTime, func);
return func;
};

private applySelectionChanged = async (
editor: TextEditor,
kind: TextEditorSelectionChangeKind | undefined,
): Promise<void> => {
// reset cursor style if needed
this.updateCursorStyle(this.main.modeManager.currentMode.name);

// wait for possible layout updates first
this.logger.debug(`${LOG_PREFIX}: Waiting for possible layout completion operation`);
await this.main.bufferManager.waitForLayoutSync();
// wait for possible change document events
this.logger.debug(`${LOG_PREFIX}: Waiting for possible document change completion operation`);
await this.main.changeManager.getDocumentChangeCompletionLock(editor.document);
this.logger.debug(`${LOG_PREFIX}: Waiting done`);

// ignore selection change caused by buffer edit
const selection = editor.selection;
const documentChange = this.main.changeManager.eatDocumentCursorAfterChange(editor.document);
if (documentChange && documentChange.isEqual(selection.active)) {
this.logger.debug(
`${LOG_PREFIX}: Skipping onSelectionChanged event since it was selection produced by doc change`,
);
} else {
this.logger.debug(
`${LOG_PREFIX}: Applying changed selection, kind: ${kind}, cursor: [${selection.active.line}, ${
selection.active.character
Expand All @@ -344,10 +369,9 @@ export class CursorManager implements Disposable, NeovimRedrawProcessable, Neovi
} else {
await this.updateNeovimVisualSelection(editor, selection);
}
},
200,
{ leading: false, trailing: true },
);
}
this.previousApplyDebounceTime = undefined;
};

/**
* Set cursor position in neovim. Coords are [0, 0] based.
Expand All @@ -365,8 +389,7 @@ export class CursorManager implements Disposable, NeovimRedrawProcessable, Neovi
`${LOG_PREFIX}: Updating cursor pos in neovim, winId: ${winId}, pos: [${pos.line}, ${pos.character}]`,
);
const vimPos = [pos.line + 1, pos.character]; // nvim_win_set_cursor is [1, 0] based
const request: [string, unknown[]][] = [["nvim_win_set_cursor", [winId, vimPos]]];
await callAtomic(this.client, request, this.logger, LOG_PREFIX);
await this.client.request("nvim_win_set_cursor", [winId, vimPos]); // a little faster
}

private async updateNeovimVisualSelection(editor: TextEditor, selection: Selection): Promise<void> {
Expand Down

0 comments on commit 7326aae

Please sign in to comment.