1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
//! Registers and paste (vim's " machinery) plus the system clipboard
//! (`+` via OSC52/wl-paste, helix's playbook). Reads run on workers;
//! results land on the drain — never a subprocess on the input path.
use super::Editor;
impl Editor {
pub(crate) fn register(&self, name: Option<char>) -> &(String, bool) {
static EMPTY: (String, bool) = (String::new(), false);
self.registers.get(&name.unwrap_or('"')).unwrap_or(&EMPTY)
}
pub(crate) fn set_register(&mut self, name: Option<char>, text: String, linewise: bool) {
// the `+` register is the system clipboard: yank/delete into it
// stages an OSC52 payload for the TUI to emit
if name == Some('+') {
self.osc52 = Some(text.clone());
}
self.registers.insert(name.unwrap_or('"'), (text, linewise));
}
/// Counted paste (vim `2p`): the register lands `count` times at
/// one position, one undo unit.
pub(crate) fn paste_n(&mut self, count: usize, before: bool) {
let (text, linewise) = self.register(None).clone();
if text.is_empty() {
return;
}
self.paste_text(text.repeat(count), linewise, before);
}
pub(crate) fn paste(&mut self, name: Option<char>, before: bool) {
if self.buf().readonly {
self.message = "readonly buffer".into();
return;
}
// `"+p`: the system clipboard is read by a provider job — never
// a subprocess on the input path (0001 §3)
if name == Some('+') {
self.clipboard_paste(before);
return;
}
let (text, linewise) = self.register(name).clone();
if text.is_empty() {
return;
}
self.paste_text(text, linewise, before);
}
/// `Space p` / `"+p`: spawn a clipboard read; the result lands in
/// drain_clipboard on a later tick.
pub(crate) fn clipboard_paste(&mut self, before: bool) {
if self.buf().readonly {
self.message = "readonly buffer".into();
return;
}
if self.clip_paste_pending.is_some() {
return; // one read in flight
}
self.clip_paste_pending = Some(before);
let tx = self.clip_tx.clone();
std::thread::spawn(move || {
let _ = tx.send(read_system_clipboard());
});
}
/// Collect clipboard reads (event-loop tick + headless settle).
pub fn drain_clipboard(&mut self) {
if self.docs.is_empty() {
return;
}
while let Ok(result) = self.clip_rx.try_recv() {
let Some(before) = self.clip_paste_pending.take() else {
continue;
};
match result {
Some(text) if !text.is_empty() => {
let linewise = text.len() > 1 && text.ends_with('\n');
self.paste_text(text, linewise, before);
}
_ => {
self.message =
"clipboard: empty or no provider (wl-paste/xclip/xsel/pbpaste)".into()
}
}
}
}
/// Insertion point + landing spot for one cursor's paste.
fn paste_points(
&self,
cursor: usize,
text_len: usize,
linewise: bool,
before: bool,
) -> (usize, usize) {
if linewise {
let line = self.buf().line_of(cursor);
let at = if before {
self.buf().line_start(line)
} else {
self.buf().line_start(line + 1)
};
(
at.min(self.buf().len_bytes()),
at.min(self.buf().len_bytes()),
)
} else {
let at = if before {
cursor
} else {
(cursor + 1).min(self.buf().len_bytes())
};
// vim: the cursor lands on the LAST pasted char, both p and P
let land = at + text_len.saturating_sub(1);
(at, land)
}
}
fn paste_text(&mut self, text: String, linewise: bool, before: bool) {
// nvim rule: every command is one undo unit — a lone paste must
// commit its own revision (it used to ride the *next* command's)
self.tx_begin();
let cursors = self.all_cursors();
if cursors.len() == 1 {
let (at, land) = self.paste_points(self.head(), text.len(), linewise, before);
self.buf_mut().insert(at, &text);
self.set_head(land);
self.clamp_cursor();
self.tx_commit();
return;
}
// multicursor paste (0013 §3): same text at every cursor,
// bottom-up so insertion points stay valid mid-batch
let primary = self.head();
let mut jobs: Vec<(usize, usize, bool)> = cursors
.into_iter()
.map(|c| {
let (at, land) = self.paste_points(c, text.len(), linewise, before);
(at, land, c == primary)
})
.collect();
jobs.sort_by_key(|j| j.0);
jobs.dedup_by_key(|j| j.0); // stacked cursors paste once
// each landing shifts by what lower insertions already added
let mut shift = 0usize;
for j in &mut jobs {
j.1 += shift;
shift += text.len();
}
for (at, _, _) in jobs.iter().rev() {
self.buf_mut().insert(*at, &text);
}
self.sels_mut()
.set_extras(jobs.iter().filter(|j| !j.2).map(|j| j.1));
self.set_head(jobs.iter().find(|j| j.2).map(|j| j.1).unwrap_or(primary));
self.normalize_cursors();
self.clamp_cursor();
self.tx_commit();
}
}
/// Read the system clipboard via the first working provider (helix's
/// playbook: wl-paste, xclip, xsel, pbpaste). Runs on a worker thread.
fn read_system_clipboard() -> Option<String> {
let providers: [(&str, &[&str]); 4] = [
("wl-paste", &[]),
("xclip", &["-selection", "clipboard", "-o"]),
("xsel", &["--clipboard", "--output"]),
("pbpaste", &[]),
];
for (cmd, args) in providers {
let Ok(out) = std::process::Command::new(cmd).args(args).output() else {
continue; // not installed
};
if out.status.success() {
return String::from_utf8(out.stdout).ok();
}
}
None
}
impl Editor {
/// Table shims (0008 stage 2): `Space y` arms the `+` register for
/// the next yank; `Space p/P` paste from the system clipboard.
pub(crate) fn clipboard_yank_pub(&mut self) {
self.pending = "\"+y".into();
}
pub(crate) fn clipboard_paste_pub(&mut self, before: bool) {
self.clipboard_paste(before);
}
/// Bracketed paste (0017): one undo unit, no key interpretation —
/// the payload is text, not keystrokes. In normal mode it behaves
/// like p; a trailing newline pastes linewise (vim's paste plugin
/// convention).
pub fn paste_bracketed(&mut self, text: &str) {
if text.is_empty() {
return;
}
if self.mode == super::Mode::Insert {
let pos = self.head();
self.tx_begin();
self.buf_mut().insert(pos, text);
self.tx_commit();
self.set_head(pos + text.len());
self.clamp_cursor();
} else {
let linewise = text.ends_with('\n');
self.paste_text(text.to_string(), linewise, false);
}
}
}