fix(buffer): keep the nvim cursor when nvim switches to another document

When nvim moves to a buffer whose document is not shown (e.g. a jump to a
global mark), vscode shows the document with the selection it saved last
time, and the layout sync then sends that selection back to nvim,
overwriting the position nvim jumped to.

Read the cursor of the current nvim window before showing the document
and apply it to the editor, so the layout sync sends nvim its own cursor.

Fixes #2699
This commit is contained in:
dualfroz committed 2026-10-03 02:24:35 +02:00
1 parent 6e63acc87e
commit ea18b8e453
2 files changed
+54 -3

No files matched your search

+34 -1
View File
@@ -10,6 +10,7 @@ import {
EventEmitter,
LogLevel,
NotebookDocument,
Position,
ProgressLocation,
Selection,
TextDocument,
@@ -30,7 +31,15 @@ import { config } from "./config";
import { EventBusData, eventBus } from "./eventBus";
import { createLogger } from "./logger";
import { MainController } from "./main_controller";
import { ManualPromise, Progress, convertByteNumToCharNum, disposeAll, fileExists, wait } from "./utils";
import {
ManualPromise,
Progress,
convertByteNumToCharNum,
convertVimPositionToEditorPosition,
disposeAll,
fileExists,
wait,
} from "./utils";
// NOTE: document and editors in vscode events and namespace are reference stable
// Integration notes:
@@ -348,6 +357,9 @@ export class BufferManager implements Disposable {
this.textDocumentToBufferId.set(doc, bufnr);
}
if (window.activeTextEditor?.document !== doc) {
// Neovim moved to this buffer itself (e.g. a jump to a global mark), so its cursor
// must win over the selection vscode restores when showing the document again
const neovimCursor = await this.getNeovimCursorInBuffer(bufnr);
const editor = await window.showTextDocument(doc, {
// viewColumn: vscode.ViewColumn.Active,
// !need to force editor to appear in the same column even if vscode 'revealIfOpen' setting is true
@@ -355,6 +367,11 @@ export class BufferManager implements Disposable {
preserveFocus: false,
preview: false,
});
if (neovimCursor) {
const active = convertVimPositionToEditorPosition(editor, neovimCursor);
editor.selections = [new Selection(active, active)];
editor.revealRange(editor.selection, TextEditorRevealType.InCenterIfOutsideViewport);
}
// force resync
this.onDidChangeEditorOptions(editor);
}
@@ -363,6 +380,22 @@ export class BufferManager implements Disposable {
}
}
/**
* Get the (0, 0)-indexed byte cursor of the current neovim window, if it shows the given buffer
*/
private async getNeovimCursorInBuffer(bufId: number): Promise<Position | undefined> {
const cursor = (await this.client.lua(
`
local buf = ...
if vim.api.nvim_get_current_buf() == buf then
return vim.api.nvim_win_get_cursor(0)
end
`,
[bufId],
)) as [number, number] | null;
return cursor ? new Position(cursor[0] - 1, cursor[1]) : undefined;
}
private handleWindowChanged = async (winId: number, token: CancellationToken): Promise<void> => {
logger.debug(`window changed, target window id: ${winId}`);
if (winId === 1000) {
+20 -2
View File
@@ -1,9 +1,10 @@
import { strict as assert } from "assert";
import path from "path";
import { NeovimClient } from "neovim";
import { Position } from "vscode";
import { Position, window } from "vscode";
import { wait } from "../../utils";
import { wait, waitUntil } from "../../utils";
import {
attachTestNvimClient,
@@ -88,4 +89,21 @@ describe("Jumplist & jump actions & marks", () => {
await wait(300);
await assertContent({ cursor: [0, 0] }, client, editor);
});
it("Global mark jump to another file keeps the mark position #2699", async () => {
const markEditor = await openTextDocument(path.join(__dirname, "../../../test_fixtures/scrolltest.txt"));
await sendVSCodeKeys("24G4lmA");
await assertContent({ cursor: [23, 4] }, client);
// Leave a different selection behind for VSCode to restore when the file is shown again
await sendVSCodeKeys("29G02l");
await assertContent({ cursor: [28, 2] }, client);
await openTextDocument(path.join(__dirname, "../../../test_fixtures/cursor-revealing.txt"));
await sendNeovimKeys(client, "`A");
await waitUntil(() => assert.equal(window.activeTextEditor?.document, markEditor.document));
// The restored selection would overwrite the mark position only after the editor is shown
await wait(1000);
await assertContent({ cursor: [23, 4] }, client);
});
});