zudo-codemirror

Type to search...

to open search from anywhere

Clipboard Integration

CreatedMar 29, 2026UpdatedMar 29, 2026Takeshi Takatsudo

Sync vim registers with the system clipboard in @replit/codemirror-vim.

Clipboard Integration

By default, vim registers in @replit/codemirror-vim are internal — yank and paste operations do not interact with the system clipboard. This page covers how to bridge that gap.

The Challenge

In a terminal vim, the "+ and "* registers connect to the system clipboard and selection buffer. In a browser-based editor, these registers are just in-memory objects with no system integration. Yanking text with "+y stores it internally but does not copy to the OS clipboard, and "+p pastes from the internal register, not from whatever the user copied elsewhere.

Register Controller API

The Vim object exposes the register controller, which manages all named registers.

import { Vim } from "@replit/codemirror-vim";

const controller = Vim.getRegisterController();
const registers = controller.registers;

Each register is an object with methods for getting and setting text. You can replace any register with a custom implementation that syncs with the system clipboard.

Creating a Clipboard Register

The following implementation creates a register object that writes to navigator.clipboard on every yank and stores text locally for paste operations.

function createClipboardRegister() {
  const register = {
    keyBuffer: [] as string[],
    insertModeChanges: [] as never[],
    searchQueries: [] as string[],
    linewise: false,
    blockwise: false,

    setText(text?: string, linewise?: boolean, blockwise?: boolean) {
      register.keyBuffer = text ? [text] : [];
      register.linewise = !!linewise;
      register.blockwise = !!blockwise;
      if (text) {
        navigator.clipboard.writeText(text).catch(console.warn);
      }
    },

    pushText(text: string, linewise?: boolean) {
      register.keyBuffer.push(text);
      register.linewise = !!linewise;
      navigator.clipboard
        .writeText(register.keyBuffer.join("\n"))
        .catch(console.warn);
    },

    pushInsertModeChanges(_changes: never) {},

    pushSearchQuery(_query: string) {},

    clear() {
      register.keyBuffer = [];
      register.linewise = false;
      register.blockwise = false;
    },

    toString() {
      return register.keyBuffer.join("\n");
    },
  };

  return register;
}

Replacing the Clipboard Registers

Replace the * and + registers with the clipboard-aware implementation.

const controller = Vim.getRegisterController();
const registers = controller.registers;
const clipboardRegister = createClipboardRegister();

registers["*"] = clipboardRegister;
registers["+"] = clipboardRegister;

After this, "+y and "*y will copy to the system clipboard, and "+p / "*p will paste from the internal buffer (which stays in sync on yank).

Clipboard=Unnamedplus Behavior

In vim, set clipboard=unnamedplus makes the default (unnamed) register alias the + register. Every yank and delete goes to the system clipboard without needing a register prefix.

To replicate this, also replace the unnamed register (").

registers['"'] = clipboardRegister;
controller.unnamedRegister = clipboardRegister;

With this in place, plain yy copies to the system clipboard and p pastes whatever was last yanked.

Syncing Clipboard on Focus

One limitation of the approach above is that text copied outside the editor (e.g., from another application) is not automatically available for p. The internal register only knows about text that was yanked inside the editor.

To handle external clipboard content, read from navigator.clipboard when the editor gains focus.

view.dom.addEventListener("focus", async () => {
  try {
    const text = await navigator.clipboard.readText();
    if (text) {
      clipboardRegister.keyBuffer = [text];
      clipboardRegister.linewise = false;
      clipboardRegister.blockwise = false;
    }
  } catch {
    // clipboard read may fail if permission is denied
  }
});

⚠️ Warning

navigator.clipboard.readText() requires the page to have clipboard-read permission. In some browsers, this is granted automatically when the document is focused. In others, the user may see a permission prompt. If permission is denied, the catch block silently handles the error.

Complete Implementation

Putting it all together.

import { Vim } from "@replit/codemirror-vim";
import { EditorView } from "@codemirror/view";

function createClipboardRegister() {
  const register = {
    keyBuffer: [] as string[],
    insertModeChanges: [] as never[],
    searchQueries: [] as string[],
    linewise: false,
    blockwise: false,

    setText(text?: string, linewise?: boolean, blockwise?: boolean) {
      register.keyBuffer = text ? [text] : [];
      register.linewise = !!linewise;
      register.blockwise = !!blockwise;
      if (text) {
        navigator.clipboard.writeText(text).catch(console.warn);
      }
    },

    pushText(text: string, linewise?: boolean) {
      register.keyBuffer.push(text);
      register.linewise = !!linewise;
      navigator.clipboard
        .writeText(register.keyBuffer.join("\n"))
        .catch(console.warn);
    },

    pushInsertModeChanges(_changes: never) {},

    pushSearchQuery(_query: string) {},

    clear() {
      register.keyBuffer = [];
      register.linewise = false;
      register.blockwise = false;
    },

    toString() {
      return register.keyBuffer.join("\n");
    },
  };

  return register;
}

function setupClipboardIntegration(view: EditorView) {
  const controller = Vim.getRegisterController();
  const registers = controller.registers;
  const clipboardRegister = createClipboardRegister();

  // Replace system clipboard registers
  registers["*"] = clipboardRegister;
  registers["+"] = clipboardRegister;

  // Make unnamed register use clipboard (clipboard=unnamedplus)
  registers['"'] = clipboardRegister;
  controller.unnamedRegister = clipboardRegister;

  // Sync external clipboard on focus
  view.dom.addEventListener("focus", async () => {
    try {
      const text = await navigator.clipboard.readText();
      if (text) {
        clipboardRegister.keyBuffer = [text];
        clipboardRegister.linewise = false;
        clipboardRegister.blockwise = false;
      }
    } catch {
      // clipboard-read permission denied
    }
  });
}

Call setupClipboardIntegration(view) after creating the EditorView.

Revision History