From ea18b8e453c1eac7bb0a23cc2608d57c46a3a7e8 Mon Sep 17 00:00:00 2001 From: dualfroz Date: Sat, 3 Oct 2026 02:24:35 +0200 Subject: [PATCH] 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 --- src/buffer_manager.ts | 35 +++++++++++++++++++++++++++++++++- src/test/integ/jumping.test.ts | 22 +++++++++++++++++++-- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/buffer_manager.ts b/src/buffer_manager.ts index c0e49bc..07b0f84 100644 --- a/src/buffer_manager.ts +++ b/src/buffer_manager.ts @@ -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 { + 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 => { logger.debug(`window changed, target window id: ${winId}`); if (winId === 1000) { diff --git a/src/test/integ/jumping.test.ts b/src/test/integ/jumping.test.ts index a01ff38..c15dd6e 100644 --- a/src/test/integ/jumping.test.ts +++ b/src/test/integ/jumping.test.ts @@ -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); + }); });