Skip to content

fix: make yank-to-system-clipboard work when the document isn't focused#287

Open
beenerdy wants to merge 1 commit into
esm7:masterfrom
beenerdy:fix/yank-to-clipboard-focus-race
Open

fix: make yank-to-system-clipboard work when the document isn't focused#287
beenerdy wants to merge 1 commit into
esm7:masterfrom
beenerdy:fix/yank-to-clipboard-focus-race

Conversation

@beenerdy

@beenerdy beenerdy commented Jul 9, 2026

Copy link
Copy Markdown

Problem

With set clipboard=unnamed (or unnamedplus), yanking sometimes fails to reach the system clipboard, even though the console logs Vim is now set to yank to system clipboard. and yankToSystemClipboard is true. It works most of the time, then intermittently stops until the editor is interacted with again. Reported in #276.

Root cause

captureYankBuffer bridges the vim yank register to the system clipboard and is fired from keyup, click, and focusin document handlers. Its two directions were asymmetric:

  • clipboard → yank (read) is wrapped in try/catch specifically to swallow DOMException: Document is not focused (the existing XXX comment).
  • yank → clipboard (write) called await win.navigator.clipboard.writeText(buf) unguarded.

The async Clipboard API only resolves when the document is focused. During any focus change (window blur, a popover/palette stealing focus, or the focusin handler firing before document.hasFocus() is true), the write rejects. The yank is silently dropped, this.lastYankBuffer is not advanced, and the rejection is unhandled. Because native Cmd/Ctrl+C uses a different, synchronous path, users see "only vim yank is broken, and only sometimes." This is not platform-specific: the focus gating is Chromium behavior, so it reproduces on macOS, Windows (#276), and Linux.

Fix

In the write branch, prefer Electron's clipboard, which is synchronous and focus-independent (the same reliability class as native copy), and fall back to navigator.clipboard.writeText where require is unavailable (e.g. mobile):

const electronClipboard = (win as any).require?.('electron')?.clipboard;
if (electronClipboard) {
    electronClipboard.writeText(buf);
} else {
    await win.navigator.clipboard.writeText(buf);
}

The whole branch is wrapped in try/catch; on failure lastYankBuffer is left unchanged so the next keyup/click/focusin retries. lastSystemClipboard is set to the value just written instead of via a second focus-gated readText().

Mobile behavior is unchanged (falls through to the async API). require is accessed as (win as any).require to avoid a bare require under noImplicitAny and to keep the commonjs plugin from resolving electron at build time.

Testing

  • npm run build succeeds (the only diagnostics are the pre-existing vim-command-done typing warnings, unrelated to this change).
  • npx vitest run passes (62/62).
  • Verified in a live Obsidian desktop renderer: a yank reaches the system clipboard via the Electron path, and still lands after a blur event that made the old async path reject.

`navigator.clipboard.writeText` rejects with "Document is not focused"
during focus changes, so with `set clipboard=unnamed` a yank silently
never reaches the system clipboard and the setting appears to stop working
(intermittently). The clipboard->yank read path already guards against this
exact error, but the yank->clipboard write path did not, and an
unhandled rejection also dropped the state update.

Prefer Electron's synchronous, focus-independent clipboard on the desktop
app, falling back to the async API where `require` is unavailable (e.g.
mobile). On any failure, leave `lastYankBuffer` unchanged so a later
keyup/click/focusin event retries the write.

Refs esm7#276
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant