1use strop_core::id::DisplayColumn;
12use strop_core::layout::LineLayout;
13use strop_core::worker::{self, Completion, FailureKind, Outcome, Ticket};
14
15use super::trace;
16use super::Editor;
17
18#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
23pub struct ClipboardKey {
24 pub document: strop_core::id::DocumentId,
25}
26
27pub type ClipboardResult = Completion<ClipboardKey, String>;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum RegisterShape {
36 Characterwise,
37 Linewise,
38 Blockwise {
41 width: DisplayColumn,
42 },
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct Register {
48 pub text: String,
49 pub shape: RegisterShape,
50}
51
52impl Register {
53 pub fn characterwise(text: impl Into<String>) -> Self {
54 Self {
55 text: text.into(),
56 shape: RegisterShape::Characterwise,
57 }
58 }
59
60 pub fn linewise(text: impl Into<String>) -> Self {
61 Self {
62 text: text.into(),
63 shape: RegisterShape::Linewise,
64 }
65 }
66
67 pub fn blockwise(text: impl Into<String>, width: DisplayColumn) -> Self {
70 Self {
71 text: text.into(),
72 shape: RegisterShape::Blockwise { width },
73 }
74 }
75
76 pub fn is_empty(&self) -> bool {
77 self.text.is_empty()
78 }
79}
80
81impl Editor {
82 pub fn register(&self, name: Option<char>) -> &Register {
83 static EMPTY: Register = Register {
84 text: String::new(),
85 shape: RegisterShape::Characterwise,
86 };
87 self.registers.get(&name.unwrap_or('"')).unwrap_or(&EMPTY)
88 }
89
90 pub(crate) fn set_register(&mut self, name: Option<char>, register: Register) {
91 if name == Some('+') {
94 self.osc52 = Some(register.text.clone());
95 }
96 self.registers.insert(name.unwrap_or('"'), register);
97 }
98
99 pub(crate) fn paste(&mut self, name: Option<char>, count: usize, before: bool) {
103 if self.buf().readonly {
104 self.message = "readonly buffer".into();
105 return;
106 }
107 if name == Some('+') {
110 self.clipboard_paste(before);
111 return;
112 }
113 let register = self.register(name).clone();
114 if register.is_empty() {
115 return;
116 }
117 self.paste_register(®ister, count, before);
118 }
119
120 fn paste_register(&mut self, register: &Register, count: usize, before: bool) {
124 match register.shape {
125 RegisterShape::Blockwise { width } => {
126 self.paste_blockwise(register, width, count, before)
127 }
128 shape @ (RegisterShape::Characterwise | RegisterShape::Linewise) => {
129 self.paste_text(register.text.repeat(count), shape, before)
130 }
131 }
132 }
133
134 pub(crate) fn clipboard_paste(&mut self, before: bool) {
137 if self.buf().readonly {
138 self.message = "readonly buffer".into();
139 return;
140 }
141 if self.clip_paste_pending.is_some() {
142 return; }
144 let request = match self.worker_ids.allocate() {
145 Ok(request) => request,
146 Err(error) => {
147 self.message = error.message;
148 return;
149 }
150 };
151 let ticket = Ticket {
152 request,
153 key: ClipboardKey {
154 document: self.current(),
155 },
156 };
157 self.clip_paste_pending = Some((before, ticket.clone()));
158 strop_trace::record_with(strop_trace::EventKind::JobStarted, || {
159 serde_json::json!({"service":"clipboard","request":request.get(),
160 "document":{"slot":self.current().index(),"generation":self.current().generation()}})
161 });
162 match self.tape.request("clipboard.read", &ticket) {
163 Ok(false) => return,
164 Ok(true) => {}
165 Err(error) => {
166 self.handle_clipboard(Completion {
167 ticket,
168 outcome: Outcome::failed(FailureKind::Protocol, error.to_string()),
169 });
170 return;
171 }
172 }
173 let tx = self.clip_tx.clone();
174 let handle = worker::spawn(
175 "strop-clipboard",
176 move |outcome| {
177 let _ = tx.send(Completion { ticket, outcome });
178 },
179 |_| read_system_clipboard(),
180 );
181 self.worker_handles.insert(request, handle);
182 }
183
184 pub(crate) fn handle_clipboard(&mut self, result: ClipboardResult) {
186 trace::services::clipboard(&result);
187 let Some((before, ticket)) = self.clip_paste_pending.as_ref() else {
188 return;
189 };
190 if *ticket != result.ticket {
191 trace::services::rejected("clipboard", "clipboard request superseded");
192 return;
193 }
194 let before = *before;
195 self.clip_paste_pending = None;
196 self.worker_handles.remove(&result.ticket.request);
197 if self.docs.is_empty() || self.current() != result.ticket.key.document {
200 self.message = "clipboard: destination changed — paste dropped".into();
201 trace::services::rejected("clipboard", &self.message);
202 return;
203 }
204 match result.outcome {
205 Outcome::Success(text) if !text.is_empty() => {
208 let register = if text.ends_with('\n') {
209 Register::linewise(text)
210 } else {
211 Register::characterwise(text)
212 };
213 self.paste_register(®ister, 1, before);
214 }
215 Outcome::Success(_) => self.message = "clipboard: empty".into(),
216 Outcome::Failed { failure, .. } => {
217 self.message = format!("clipboard: {}", failure.message)
218 }
219 Outcome::Cancelled(_) => {}
221 }
222 }
223
224 fn paste_points(&self, cursor: usize, text: &str, shape: RegisterShape, before: bool) -> Spot {
229 match shape {
230 RegisterShape::Linewise => {
231 let line = self.buf().line_of(cursor);
232 let (at, prefix) = if before {
233 (self.buf().line_start(line), 0usize)
234 } else if line + 1 < self.buf().len_lines() {
235 (self.buf().line_start(line + 1), 0usize)
236 } else {
237 let ends_break = self.buf().len_bytes() > 0
242 && self.buf().byte(self.buf().len_bytes() - 1) == b'\n';
243 let prefix = if ends_break {
244 0
245 } else {
246 self.newline_str().len()
247 };
248 (self.buf().len_bytes(), prefix)
249 };
250 let row = &text[prefix.min(text.len())..];
252 let blank = row
253 .chars()
254 .take_while(|c| *c == ' ' || *c == '\t')
255 .map(char::len_utf8)
256 .sum::<usize>()
257 .min(row.len());
258 Spot {
259 at,
260 land: at + prefix + blank,
261 text: if prefix == 0 {
262 text.to_string()
263 } else {
264 format!("{}{}", self.newline_str(), text)
265 },
266 }
267 }
268 RegisterShape::Characterwise => {
269 let at = if before {
270 cursor
271 } else {
272 self.buf()
275 .ceil_boundary(cursor + 1)
276 .min(self.buf().len_bytes())
277 };
278 let last = text.chars().next_back().map_or(0, char::len_utf8);
281 Spot {
282 at,
283 land: at + text.len().saturating_sub(last),
284 text: text.to_string(),
285 }
286 }
287 RegisterShape::Blockwise { .. } => unreachable!("blockwise pastes its own way"),
288 }
289 }
290
291 fn paste_text(&mut self, text: String, shape: RegisterShape, before: bool) {
292 self.tx_begin();
295 let cursors = self.all_cursors();
296 if cursors.len() == 1 {
297 let spot = self.paste_points(self.head(), &text, shape, before);
298 self.buf_mut().insert(spot.at, &spot.text);
299 self.set_head(spot.land);
300 self.clamp_cursor();
301 self.tx_commit();
302 return;
303 }
304 let primary = self.head();
309 let mut jobs: Vec<(usize, usize, bool, String)> = cursors
310 .into_iter()
311 .map(|c| {
312 let spot = self.paste_points(c, &text, shape, before);
313 (spot.at, spot.land, c == primary, spot.text)
314 })
315 .collect();
316 jobs.sort_by_key(|j| j.0);
317 jobs.dedup_by_key(|j| j.0); let mut shift = 0usize;
320 for j in &mut jobs {
321 j.1 += shift;
322 shift += j.3.len();
323 }
324 for (at, _, _, insert) in jobs.iter().rev() {
325 self.buf_mut().insert(*at, insert);
326 }
327 self.sels_mut()
328 .set_extras(jobs.iter().filter(|j| !j.2).map(|j| j.1));
329 self.set_head(jobs.iter().find(|j| j.2).map(|j| j.1).unwrap_or(primary));
330 self.normalize_cursors();
331 self.clamp_cursor();
332 self.tx_commit();
333 }
334
335 fn paste_blockwise(
344 &mut self,
345 register: &Register,
346 width: DisplayColumn,
347 count: usize,
348 before: bool,
349 ) {
350 let tab = self.config.tab_size.max(1);
351 self.sels_mut().collapse_extras();
354 let cursor = self.head();
355 let line = self.buf().line_of(cursor);
356 let head_row = self.buf().line_text(line);
357 let layout = LineLayout::build(&head_row, tab);
358 let col = self.buf().col_of(cursor);
359 let column = if before || head_row.is_empty() {
360 layout.cell_at_byte(col)
363 } else {
364 let cluster = layout.spans().iter().rev().find(|s| s.byte <= col);
366 layout.cell_at_byte(col) + cluster.map_or(0, |s| s.width)
367 };
368 let rows: Vec<&str> = register.text.split('\n').collect();
369 let sep = self.newline_str();
370 let len = self.buf().len_bytes();
371 let ends_break = len > 0 && self.buf().byte(len - 1) == b'\n';
372 let tail_from = if ends_break {
376 self.buf().len_lines().saturating_sub(1)
377 } else {
378 self.buf().len_lines()
379 };
380 let mut edits: Vec<(usize, String)> = Vec::with_capacity(rows.len() + 1);
381 let mut tail = String::new();
382 let mut row0_land = cursor;
383 for (i, row) in rows.iter().enumerate() {
384 let target = line + i;
385 if target < tail_from {
386 let start = self.buf().line_start(target);
387 let end = self.buf().line_end(target);
388 let text = self.buf().line_text(target);
389 let row_layout = LineLayout::build(&text, tab);
390 let mid_row = row_layout.width.get() > column.get();
395 let content = block_row(row, width, count, mid_row, tab);
396 let (at, content) = if row_layout.width.get() < column.get() {
397 let pad = " ".repeat(column.get() - row_layout.width.get());
399 (end, format!("{pad}{content}"))
400 } else {
401 (start + row_layout.byte_at_cell(column), content)
404 };
405 if i == 0 {
406 row0_land = at;
407 }
408 edits.push((at, content));
409 } else {
410 if !(tail.is_empty() && ends_break && target == tail_from) {
411 tail.push_str(sep);
414 }
415 if column.get() > 0 {
416 tail.push_str(&" ".repeat(column.get()));
417 }
418 tail.push_str(&block_row(row, width, count, false, tab));
419 if i == 0 {
420 row0_land = len
421 + if ends_break && target == tail_from {
422 0
423 } else {
424 sep.len()
425 };
426 }
427 }
428 }
429 if !tail.is_empty() {
430 tail.push_str(sep);
433 edits.push((len, tail));
434 }
435 self.tx_begin();
436 for (at, text) in edits.iter().rev() {
437 self.buf_mut().insert(*at, text);
438 }
439 self.set_head(row0_land);
440 self.clamp_cursor();
441 self.tx_commit();
442 }
443}
444
445struct Spot {
449 at: usize,
450 land: usize,
451 text: String,
452}
453
454fn block_row(row: &str, width: DisplayColumn, count: usize, mid_row: bool, tab: usize) -> String {
459 let mut unit = row.to_string();
460 if mid_row {
461 let cells = LineLayout::build(row, tab).width.get();
462 let pad = width.get().saturating_sub(cells);
463 unit.push_str(&" ".repeat(pad));
464 }
465 unit.repeat(count)
466}
467
468fn read_system_clipboard() -> Outcome<String> {
475 let providers: [(&str, &[&str]); 4] = [
476 ("wl-paste", &[]),
477 ("xclip", &["-selection", "clipboard", "-o"]),
478 ("xsel", &["--clipboard", "--output"]),
479 ("pbpaste", &[]),
480 ];
481 let mut failed = Vec::new();
482 for (cmd, args) in providers {
483 let output = match std::process::Command::new(cmd)
484 .args(args)
485 .stdin(std::process::Stdio::null())
486 .stdout(std::process::Stdio::piped())
487 .stderr(std::process::Stdio::null())
488 .output()
489 {
490 Ok(output) => output,
491 Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
493 Err(error) => {
494 return Outcome::failed(FailureKind::Spawn, format!("{cmd}: {error}"));
495 }
496 };
497 if !output.status.success() {
498 failed.push(format!("{cmd}: exited {}", output.status));
499 continue;
500 }
501 return match String::from_utf8(output.stdout) {
502 Ok(text) => Outcome::Success(text),
503 Err(error) => Outcome::failed(
504 FailureKind::Io,
505 format!("{cmd}: clipboard is not UTF-8: {error}"),
506 ),
507 };
508 }
509 let mut message = String::from("no clipboard provider succeeded");
510 if !failed.is_empty() {
511 message.push_str(&format!(" ({})", failed.join("; ")));
512 }
513 Outcome::failed(FailureKind::Unavailable, message)
514}
515
516impl Editor {
517 pub(crate) fn clipboard_yank_pub(&mut self) {
520 self.walker
521 .begin_operator(strop_grammar::Op::Yank, Some('+'), None);
522 }
523 pub(crate) fn clipboard_paste_pub(&mut self, before: bool) {
524 self.clipboard_paste(before);
525 }
526 pub fn paste_bracketed(&mut self, text: &str) {
532 if text.is_empty() {
533 return;
534 }
535 if self.pending.is_active() {
536 self.feed_pending_event(super::pending::PendingEvent::Paste(text.to_owned()));
537 return;
538 }
539 if self.picker_open() {
540 self.paste_picker(text);
541 return;
542 }
543 if self.mode == super::Mode::Insert {
544 let pos = self.head();
545 self.tx_begin();
546 self.buf_mut().insert(pos, text);
547 self.tx_commit();
548 self.set_head(pos + text.len());
549 self.clamp_cursor();
550 } else {
551 let register = if text.ends_with('\n') {
552 Register::linewise(text)
553 } else {
554 Register::characterwise(text)
555 };
556 self.paste_register(®ister, 1, false);
557 }
558 }
559}