feat(vim): yank to system clipboard (#5463)#9733
Conversation
Add support for `set clipboard=unnamedplus` (and `clipboard=unnamed`) in vimrc. Yanking in vim normal mode now writes to the system clipboard; `p`/`P` reads from the system clipboard before pasting.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
@kirangadhave I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
Pull request overview
This PR adds limited support for :set clipboard=unnamedplus / clipboard=unnamed in the frontend vim keymap integration so that yanks sync into the system clipboard and p/P pulls from the system clipboard before pasting.
Changes:
- Extend vimrc parsing to recognize the
setcommand with anoptionargument. - Apply
set clipboard=unnamedplus|unnamedat runtime by enabling clipboard synchronization with the vim unnamed register. - Intercept
p/Pto read the system clipboard into the unnamed register before delegating to the vim key handler.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| frontend/src/core/codemirror/keymaps/vimrc.ts | Adds set as a known vimrc command with an option argument. |
| frontend/src/core/codemirror/keymaps/vim.ts | Enables clipboard sync via register patching and intercepts p/P to paste from system clipboard. |
| frontend/src/core/codemirror/keymaps/tests/vimrc.test.ts | Adds tests for parsing set clipboard=unnamedplus and erroring on set with no args. |
| unnamedRegister.setText = (text, linewise, blockwise) => { | ||
| origSetText(text, linewise, blockwise); | ||
| if (text) { | ||
| navigator.clipboard?.writeText(text).catch(() => {}); | ||
| } | ||
| }; |
| unnamedRegister.pushText = (text, linewise) => { | ||
| origPushText(text, linewise); | ||
| if (text) { | ||
| navigator.clipboard | ||
| ?.writeText(unnamedRegister.toString()) | ||
| .catch(() => {}); | ||
| } | ||
| }; |
| const origSet = _origSetTextFn; | ||
| navigator.clipboard | ||
| ?.readText() | ||
| .then((text) => { | ||
| if (text) { | ||
| origSet(text, false, false); | ||
| } | ||
| Vim.handleKey(cm, key, "mapping"); | ||
| }) | ||
| .catch(() => { | ||
| // Clipboard unavailable — fall back to whatever is in the register. | ||
| Vim.handleKey(cm, key, "mapping"); | ||
| }); | ||
| return true; |
| const isFirefox = /firefox/i.test(navigator.userAgent); | ||
| Logger.log( | ||
| `[vim] clipboard sync (unnamedplus) enabled${isFirefox ? " — Firefox will show a paste-permission popup on each p/P; workaround: set dom.events.testing.asyncClipboard=true in about:config (at your own risk)" : ""}`, | ||
| ); |
There was a problem hiding this comment.
I agree with this comment. We should just say that Firefox may ask for permissions. Users can figure out how to workaround it if they want. We don't want to prompt them on bypassing clipboard security.
There was a problem hiding this comment.
1 issue found across 3 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="frontend/src/core/codemirror/keymaps/vim.ts">
<violation number="1" location="frontend/src/core/codemirror/keymaps/vim.ts:334">
P2: `p`/`P` can paste stale register contents when system clipboard text is empty because empty clipboard values are ignored.</violation>
</file>
Architecture diagram
sequenceDiagram
participant User as User
participant VimRC as .vimrc File
participant VimParser as vimrc Parser
participant VimExt as Vim Extension
participant CM as CodeMirror
participant Register as CodeMirror Register
participant Clipboard as System Clipboard (navigator.clipboard)
User->>VimRC: writes "set clipboard=unnamedplus"
VimExt->>VimParser: parseVimrc()
VimParser-->>VimExt: commands [{name:"set", args:{option:"clipboard=unnamedplus"}}]
VimExt->>VimExt: enableVimClipboardSync()
Note over VimExt: Patches registerController.unnamedRegister.setText() and .pushText()
VimExt->>Register: replaces setText/pushText with wrapper
Register-->>VimExt: patched
Note over User,VimExt: Yank in normal mode
User->>Vim: executes y (yank) in normal mode
Vim->>Register: setText("selected-text")
Register->>Register: origSetText("selected-text")
Register->>Clipboard: navigator.clipboard.writeText("selected-text")
Clipboard-->>Register: ok (async)
Note over User,VimExt: Paste in normal mode (p/P)
User->>Vim: presses p (paste)
Vim->>CM: interceptVimPaste(view, "p")
alt clipboard sync enabled AND in normal mode AND no explicit register
CM->>Clipboard: navigator.clipboard.readText()
Clipboard-->>CM: "clipboard-text" (async)
CM->>Register: origSetText("clipboard-text", false, false)
CM->>Vim: Vim.handleKey(cm, "p", "mapping")
Vim->>Register: reads unnamedRegister
Register-->>Vim: "clipboard-text"
Vim-->>User: pastes content from system clipboard
else clipboard sync disabled OR explicit register OR not normal mode
CM-->>Vim: returns false (fallback to normal vim paste)
Vim->>Register: reads unnamedRegister (unchanged)
Register-->>Vim: stored text
Vim-->>User: pastes register content (no clipboard sync)
else clipboard read fails
CM->>Vim: Vim.handleKey(cm, key, "mapping")
Vim->>Register: reads unnamedRegister (unchanged)
Register-->>Vim: stored text
Vim-->>User: pastes register content (clipboard read failed)
end
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
|
|
||
| unnamedRegister.setText = (text, linewise, blockwise) => { | ||
| origSetText(text, linewise, blockwise); | ||
| if (text) { |
There was a problem hiding this comment.
P2: p/P can paste stale register contents when system clipboard text is empty because empty clipboard values are ignored.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/core/codemirror/keymaps/vim.ts, line 334:
<comment>`p`/`P` can paste stale register contents when system clipboard text is empty because empty clipboard values are ignored.</comment>
<file context>
@@ -273,6 +305,87 @@ function isMacroActive() {
+
+ unnamedRegister.setText = (text, linewise, blockwise) => {
+ origSetText(text, linewise, blockwise);
+ if (text) {
+ navigator.clipboard?.writeText(text).catch(() => {});
+ }
</file context>
kirangadhave
left a comment
There was a problem hiding this comment.
Thanks @LioSergent for the PR. The PR is handrolling features codemirror-vim already supports in some way.
The library already syncs the system clipboard with the + register in both directions:
RegisterController.pushTextcallsnavigator.clipboard.writeTextwhen the register is+- the
pasteaction reads from the system clipboard when the register is+
So "+y and "+p work as-is today.
The things in this PR that reimplement existing functionality:
enableVimClipboardSyncmonkey-patchesVim.getVimGlobalState_().registerController.unnamedRegister. This is a no-go, because we're patching a codemirror internal. It can break in future updates, and there's no test catching it, so a codemirror update would give us a silent failure with just a warning log.interceptVimPastereads the clipboard and re-dispatchesVim.handleKeyoutside the paste pipeline. I'm not sure it behaves correctly with count prefixes (3p) and dot-repeat. And"+palready does the async read internally, so this isn't needed.
The only thing actually missing is the aliasing: making a bare y/p use + without the prefix. The library has no option for that, but we shouldn't patch it via internals. Two ways forward:
-
Add a
clipboardoption upstream in codemirror-vim. The plumbing (+register,defineOption,RegisterController) is already there, so it belongs in the library. There's an open issue for this: replit/codemirror-vim#142 -
Remap the operators onto the
+register via the publicVim.noremapAPI (the same one vimrcnoremaplines already use), e.g.:for (const ctx of ["normal", "visual"] as const) { for (const op of ["y", "d", "c", "s", "x"]) { Vim.noremap(op, `"+${op}`, ctx); } } Vim.noremap("p", '"+p', "normal"); Vim.noremap("P", '"+P', "normal");
This lets the library do all the clipboard I/O and drops the internal patching and the p/P interceptor entirely. The one tradeoff is that an explicit register prefix on a mapped key (
"ay) would still route to+, which is acceptable for an opt-in setting.
Either way, I'd drop the internal function patching approach.
| return; | ||
| } | ||
| const unnamedRegister = | ||
| Vim.getVimGlobalState_()?.registerController?.unnamedRegister; |
There was a problem hiding this comment.
this is an internal codemirror function. it can break with future updates, we shouldn't rely on internals.
Add support for
set clipboard=unnamedplus(andclipboard=unnamed) in vimrc. Yanking in vim normal mode now writes to the system clipboard;p/Preads from the system clipboard before pasting.📝 Summary
Closes #5463
📋 Pre-Review Checklist
✅ Merge Checklist