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
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 FernTech
//! System clipboard integration for the rich text editor.
//!
//! Three free functions — `copy`, `cut`, `paste` — implement the
//! in-process rich fragment preservation pattern of the godot reference's
//! `copy_rich` / `paste_rich` / `cut_rich` (`godot-rich-text/src/rich_text_edit.rs`).
//! The reference is cited by symbol throughout this module rather than by
//! line: it is a sibling checkout that moves on its own, and every line number
//! ever written down here had drifted onto unrelated code.
//!
//! The functions talk to a `ClipboardHandle` retrieved via
//! `EventContext::app_state::<ClipboardHandle>()`. In headless tests
//! or builds without the `teksilo-app/clipboard` feature the handle is
//! absent and copy/cut/paste silently no-op — the same behaviour as
//! trying to paste an image into a pure-text editor. No panic, no
//! error propagation — the user sees nothing happen and the command
//! filter's UI affordance drives the expectation.
//!
//! Self-round-trip detection embeds an opaque marker as an HTML
//! comment at the head of the clipboard HTML payload and re-reads it
//! on paste. Plain-text equality alone is ambiguous — two different
//! apps can publish identical plain text with different formatting —
//! so the marker is the reliable signal that *this* editor wrote the
//! clipboard. The marker is regenerated on every copy/cut so a stale
//! state from an earlier session can never accidentally match a later
//! external copy whose plain text happens to coincide.
//!
//! Plain-text equality does serve as a **last** resort, for the one case the
//! marker cannot reach: a clipboard backend that carries no HTML at all. There
//! is no foreign rich payload to be confused with then, so matching the text
//! this editor last copied is unambiguous, and without it an intra-app
//! copy/paste would lose its formatting on every such backend.
use ;
use EventContext;
use ClipboardHandle;
use EditorState;
/// HTML comment prefix embedded in the clipboard payload to flag a
/// self-copy. The trailing hex token is regenerated per copy.
const MARKER_PREFIX: &str = "<!--teksilo-rtc:";
const MARKER_SUFFIX: &str = "-->";
/// Copy the current selection to the system clipboard. No-op when
/// there is no selection — matches editor convention (the menu item
/// and the Ctrl+C shortcut both stay silent rather than capturing an
/// empty fragment).
///
/// Writes both HTML and plain-text payloads so rich paste works in
/// any other application that understands `text/html` (Firefox,
/// Word, Google Docs, Apple Notes, …). `DocumentFragment::to_html`
/// is a lossless-enough serialisation to survive the round-trip
/// through arboard's platform backends. Backends without HTML
/// support degrade gracefully — the `set_html` default body writes
/// just the plain-text alternative.
///
/// The rich fragment + plain text are also stashed on editor state
/// for self-round-trip detection during paste: an intra-editor
/// copy/paste pair re-inserts the original `DocumentFragment`
/// rather than round-tripping through HTML (cheaper and bit-exact).
pub
/// Generate a per-copy marker from the wall clock, in nanoseconds.
///
/// Not a cryptographic identifier, and not a unique one either: the clock is
/// not monotonic, so a step backwards can repeat a value. Neither matters. The
/// only requirement is that an *unrelated* application cannot plausibly emit
/// the same string, and a 32-hex-digit token behind a `teksilo-rtc:` prefix
/// clears that by a wide margin. Repeating our own token costs nothing either:
/// the fragment it guards was overwritten by the same copy that regenerated
/// it.
/// How far into a clipboard payload the marker is still ours.
///
/// Long enough for any plausible wrapper preamble, short enough that a
/// `teksilo-rtc:` comment sitting in the *body* of a foreign document cannot
/// masquerade as one.
const MARKER_SEARCH_WINDOW: usize = 1024;
/// Extract the marker token from a clipboard HTML payload written by
/// `copy`. Returns `None` for any payload that doesn't carry
/// `<!--teksilo-rtc:...-->` near its head — which covers every payload emitted
/// by any other app.
///
/// Near its head, not at byte 0: platform clipboards are entitled to wrap what
/// they are handed, and macOS does. `arboard`'s AppKit backend puts every
/// payload inside `<html><head><meta …></head><body>…</body></html>` on write
/// and hands the wrapper straight back on read, so a strict prefix test never
/// matched there and every intra-editor copy/paste on macOS silently fell
/// through to re-parsing its own HTML. (X11 and Wayland round-trip verbatim;
/// Windows' CF_HTML reader slices to `StartFragment`, which lands exactly on
/// the marker.)
/// Cut the current selection: copy first, then remove. `pending_text_changed`
/// is set so the debounce drain publishes a `text_changed` command once
/// the 150 ms window closes. No-op with no selection.
pub
/// Paste from the system clipboard. Prefers richer payloads, in order:
///
/// 1. **Self-round-trip rich fragment** — if the clipboard's HTML payload
/// carries the marker this editor's last copy embedded, reinsert the
/// stored `DocumentFragment` so intra-editor formatting round-trips
/// losslessly (retains table cells, heading levels, spans that don't
/// serialise into HTML losslessly). The marker, not plain-text equality:
/// two applications can publish identical text with different formatting.
/// 2. **External HTML payload** — if the clipboard carries `text/html`
/// / `CF_HTML` / `public.html`, parse it into a `DocumentFragment`
/// via text-document and insert. This is the path that makes
/// rich paste *from another app* work (Firefox, Word, Google Docs,
/// etc.).
/// 3. **Self-round-trip by plain text** — with no HTML payload at all there
/// is no foreign rich content to be confused with, so text identical to
/// what this editor last copied reinserts the stored fragment. Only this
/// keeps formatting on a clipboard backend that cannot carry HTML, where
/// step 1 can never fire.
/// 4. **Plain-text fallback** — when no rich path applies, split
/// the clipboard text on `\n` / `\r\n` / `\r` and insert as separate
/// blocks. Without the split, a multi-line clipboard payload would
/// collapse into one block with literal newline scalars —
/// `text-document::TextCursor::insert_text` never splits blocks on
/// its own.
///
/// Clears any existing selection after insertion so the caret sits at
/// the end of the pasted content rather than keeping the newly inserted
/// range selected — matches godot behaviour.
pub
/// Reinsert the fragment stashed by the last copy/cut, reporting how much
/// arrived. `false` when there is nothing stashed, so the caller falls through
/// to the next payload shape.
/// Paste plain text only, bypassing any rich payload. Bound to
/// Ctrl+Shift+V / ⌘⇧V and exposed from the default context menu as
/// "Paste Unformatted". Skips both the self-round-trip fragment
/// reinsertion and the HTML parse path — the user explicitly asked
/// for plain text, so even if the clipboard has a richer payload
/// we insert `get_text()` verbatim.
pub
/// Whether a [`paste`] would insert anything — `true` iff the system
/// clipboard carries text **or** an HTML payload.
///
/// This is the union of the shapes `paste` can actually consume: its
/// self-round-trip (step 1) and external-HTML (step 2) branches insert
/// from `get_html()` independently of any plain-text companion, and the
/// plain-text branch (step 3) handles the rest. Probing only
/// `has_text()` would wrongly report an HTML-only clipboard — an app
/// that published `text/html` without a `text/plain` alternative — as
/// un-pasteable, greying out a Paste command that would in fact succeed.
///
/// Returns `false` when no clipboard backend is installed (headless /
/// feature-off builds), matching `paste`'s own silent no-op. `has_html`
/// can round-trip to the X11 selection owner, so this is a
/// menu-build-time query, never a per-frame one.
pub
/// Insert plain text that may contain line breaks, splitting on
/// `\n` into separate blocks. `text-document::TextCursor::insert_text`
/// treats `\n` as a literal scalar inside one block, so pasting a
/// multi-line clipboard payload without this split leaves every line
/// fused into one paragraph. Normalises `\r\n` and bare `\r` first so
/// Windows- and classic-Mac clipboards round-trip cleanly.
/// How many characters an insertion actually added, measured around it.
///
/// For the two rich paste paths, where what goes in is a fragment or a parsed
/// HTML tree and the plain-text length is not in hand. A bare before/after delta
/// would **undercount every paste over a selection** — the insert removes the
/// selection first, so the delta is what arrived minus what went — which is the
/// common case rather than a corner. Adding the replaced length back makes it
/// exact:
///
/// ```text
/// after = before - replaced + arrived ⇒ arrived = after - before + replaced
/// ```