1
//! Pure functions for editing a `Vec<InlineContent>` based on selections.
2
//!
3
//! Entry points: [`edit_text`] (single edit, multiple cursors),
4
//! [`edit_text_multi`] (per-cursor text), and [`inspect_delete`]
5
//! (preview what a delete would remove).
6

            
7
use std::sync::Arc;
8

            
9
use azul_core::selection::{
10
    CursorAffinity, GraphemeClusterId, Selection, SelectionRange, TextCursor,
11
};
12

            
13
use crate::text3::cache::{ContentIndex, InlineContent, StyledRun};
14

            
15
/// An enum representing a single text editing action.
16
#[derive(Debug, Clone)]
17
pub enum TextEdit {
18
    /// Insert the given string at the cursor position.
19
    Insert(String),
20
    /// Delete one grapheme cluster before the cursor (Backspace).
21
    DeleteBackward,
22
    /// Delete one grapheme cluster after the cursor (Delete key).
23
    DeleteForward,
24
}
25

            
26
/// The primary entry point for text modification. Takes the current content and selections,
27
/// applies an edit, and returns the new content and the resulting cursor positions.
28
572
pub fn edit_text(
29
572
    content: &[InlineContent],
30
572
    selections: &[Selection],
31
572
    edit: &TextEdit,
32
572
) -> (Vec<InlineContent>, Vec<Selection>) {
33
572
    if selections.is_empty() {
34
        return (content.to_vec(), Vec::new());
35
572
    }
36

            
37
572
    let mut new_content = content.to_vec();
38
572
    let mut new_selections = Vec::new();
39

            
40
    // To handle multiple cursors correctly, we must process edits
41
    // from the end of the document to the beginning. This ensures that
42
    // earlier edits do not invalidate the indices of later edits.
43
572
    let mut sorted_selections = selections.to_vec();
44
572
    sorted_selections.sort_by(|a, b| {
45
        let cursor_a = match a {
46
            Selection::Cursor(c) => c,
47
            Selection::Range(r) => &r.start,
48
        };
49
        let cursor_b = match b {
50
            Selection::Cursor(c) => c,
51
            Selection::Range(r) => &r.start,
52
        };
53
        cursor_b.cluster_id.cmp(&cursor_a.cluster_id) // Reverse sort
54
    });
55

            
56
1144
    for selection in sorted_selections {
57
572
        let (mut temp_content, new_cursor) =
58
572
            apply_edit_to_selection(&new_content, &selection, edit);
59

            
60
        // When we insert/delete text, we need to adjust all previously-processed cursors
61
        // that come after this edit position in the same run
62
572
        let edit_run = match selection {
63
572
            Selection::Cursor(c) => c.cluster_id.source_run,
64
            Selection::Range(r) => r.start.cluster_id.source_run,
65
        };
66
572
        let edit_byte = match selection {
67
572
            Selection::Cursor(c) => c.cluster_id.start_byte_in_run,
68
            Selection::Range(r) => r.start.cluster_id.start_byte_in_run,
69
        };
70

            
71
        // Calculate the byte offset change
72
572
        let byte_offset_change: i32 = match edit {
73
572
            TextEdit::Insert(text) => text.len() as i32,
74
            TextEdit::DeleteBackward | TextEdit::DeleteForward => {
75
                // For simplicity, assume 1 grapheme deleted = some bytes
76
                // A full implementation would track actual bytes deleted
77
                -1
78
            }
79
        };
80

            
81
        // Adjust all previously-processed cursors in the same run that come after this position
82
572
        for prev_selection in new_selections.iter_mut() {
83
            if let Selection::Cursor(cursor) = prev_selection {
84
                if cursor.cluster_id.source_run == edit_run
85
                    && cursor.cluster_id.start_byte_in_run >= edit_byte
86
                {
87
                    cursor.cluster_id.start_byte_in_run =
88
                        (cursor.cluster_id.start_byte_in_run as i32 + byte_offset_change).max(0)
89
                            as u32;
90
                }
91
            }
92
        }
93

            
94
572
        new_content = temp_content;
95
572
        new_selections.push(Selection::Cursor(new_cursor));
96
    }
97

            
98
    // The new selections were added in reverse order, so we reverse them back.
99
572
    new_selections.reverse();
100

            
101
572
    (new_content, new_selections)
102
572
}
103

            
104
/// Applies a single edit to a single selection.
105
///
106
/// When the selection is a Range:
107
/// - `Insert`: deletes the range, then inserts text at the collapsed cursor
108
/// - `DeleteBackward`/`DeleteForward`: deletes the range ONLY (the range
109
///   deletion replaces the character-level delete — pressing Backspace with
110
///   a selection should remove the selection, not the selection + 1 char)
111
572
pub fn apply_edit_to_selection(
112
572
    content: &[InlineContent],
113
572
    selection: &Selection,
114
572
    edit: &TextEdit,
115
572
) -> (Vec<InlineContent>, TextCursor) {
116
572
    let mut new_content = content.to_vec();
117

            
118
572
    match selection {
119
        Selection::Range(range) => {
120
            // Delete the range first
121
            let (content_after_delete, cursor_pos) = delete_range(&new_content, range);
122
            match edit {
123
                // Insert: replace the deleted range with new text
124
                TextEdit::Insert(text_to_insert) => {
125
                    let mut c = content_after_delete;
126
                    insert_text(&mut c, &cursor_pos, text_to_insert)
127
                }
128
                // Delete: range deletion is sufficient — don't delete again
129
                TextEdit::DeleteBackward | TextEdit::DeleteForward => {
130
                    (content_after_delete, cursor_pos)
131
                }
132
            }
133
        }
134
572
        Selection::Cursor(cursor) => {
135
572
            match edit {
136
572
                TextEdit::Insert(text_to_insert) => {
137
572
                    insert_text(&mut new_content, cursor, text_to_insert)
138
                }
139
                TextEdit::DeleteBackward => delete_backward(&mut new_content, cursor),
140
                TextEdit::DeleteForward => delete_forward(&mut new_content, cursor),
141
            }
142
        }
143
    }
144
572
}
145

            
146
/// Absolute byte offset of a cursor within its run's text, honoring affinity.
147
///
148
/// `Leading` = at the start of the referenced grapheme cluster; `Trailing` =
149
/// after it. This mirrors the affinity handling in `insert_text` /
150
/// `delete_backward` / `delete_forward`, and is what lets a select-all range
151
/// (whose end cursor is `Trailing` on the last cluster) cover the whole text.
152
pub(crate) fn cursor_byte_offset_in_run(text: &str, cursor: &TextCursor) -> usize {
153
    use unicode_segmentation::UnicodeSegmentation;
154
    let csb = cursor.cluster_id.start_byte_in_run as usize;
155
    match cursor.affinity {
156
        CursorAffinity::Leading => csb.min(text.len()),
157
        CursorAffinity::Trailing => {
158
            if csb >= text.len() {
159
                text.len()
160
            } else {
161
                text[csb..]
162
                    .grapheme_indices(true)
163
                    .next()
164
                    .map(|(_, g)| csb + g.len())
165
                    .unwrap_or(text.len())
166
            }
167
        }
168
    }
169
}
170

            
171
/// Deletes the content within a given range.
172
pub fn delete_range(
173
    content: &[InlineContent],
174
    range: &SelectionRange,
175
) -> (Vec<InlineContent>, TextCursor) {
176
    // This is a highly complex function. A full implementation needs to handle:
177
    //
178
    // - Deletions within a single text run.
179
    // - Deletions that span across multiple text runs.
180
    // - Deletions that include non-text items like images.
181
    //
182
    // For now, we provide a simplified version that handles deletion within a
183
    // single run.
184

            
185
    let mut new_content = content.to_vec();
186
    let start_run_idx = range.start.cluster_id.source_run as usize;
187
    let end_run_idx = range.end.cluster_id.source_run as usize;
188

            
189
    // The range may be "backward" (start after end) when the user selected
190
    // right-to-left, e.g. Shift+Home or Shift+Left. Normalize to [lo, hi] so the
191
    // deletion is direction-agnostic. The old `start_byte <= end_byte` guard
192
    // skipped the drain for backward ranges, so Delete/Backspace (and type-to-
193
    // replace) silently did nothing on such selections.
194
    let mut cursor_after = range.start;
195
    if start_run_idx == end_run_idx {
196
        if let Some(InlineContent::Text(run)) = new_content.get_mut(start_run_idx) {
197
            let a = cursor_byte_offset_in_run(&run.text, &range.start);
198
            let b = cursor_byte_offset_in_run(&run.text, &range.end);
199
            let lo = a.min(b);
200
            let hi = a.max(b);
201
            if hi <= run.text.len() && lo < hi {
202
                run.text.drain(lo..hi);
203
                // Collapse the caret to the start of the deleted region (the low
204
                // end), regardless of the original selection direction.
205
                cursor_after = TextCursor {
206
                    cluster_id: GraphemeClusterId {
207
                        source_run: start_run_idx as u32,
208
                        start_byte_in_run: lo as u32,
209
                    },
210
                    affinity: CursorAffinity::Leading,
211
                };
212
            }
213
        }
214
    } else {
215
        // TODO: Handle multi-run deletion
216
    }
217

            
218
    (new_content, cursor_after) // caret at the start of the deleted range
219
}
220

            
221
/// Inserts text at a cursor position.
222
/// 
223
/// The cursor's affinity determines the exact insertion point:
224
/// - `Leading`: Insert at the start of the referenced cluster (start_byte_in_run)
225
/// - `Trailing`: Insert at the end of the referenced cluster (after the grapheme)
226
572
pub fn insert_text(
227
572
    content: &mut Vec<InlineContent>,
228
572
    cursor: &TextCursor,
229
572
    text_to_insert: &str,
230
572
) -> (Vec<InlineContent>, TextCursor) {
231
    use unicode_segmentation::UnicodeSegmentation;
232
    
233
572
    let mut new_content = content.clone();
234
572
    let run_idx = cursor.cluster_id.source_run as usize;
235
572
    let cluster_start_byte = cursor.cluster_id.start_byte_in_run as usize;
236

            
237
572
    if let Some(InlineContent::Text(run)) = new_content.get_mut(run_idx) {
238
        // Calculate the actual insertion byte offset based on affinity
239
572
        let byte_offset = match cursor.affinity {
240
            CursorAffinity::Leading => {
241
                // Insert at the start of the cluster
242
264
                cluster_start_byte
243
            },
244
            CursorAffinity::Trailing => {
245
                // Insert at the end of the cluster - find the next grapheme boundary
246
                // We need to find where this grapheme cluster ends
247
308
                if cluster_start_byte >= run.text.len() {
248
                    // Cursor is at/past end of run - insert at end
249
                    run.text.len()
250
                } else {
251
                    // Find the grapheme that starts at cluster_start_byte and get its end
252
308
                    run.text[cluster_start_byte..]
253
308
                        .grapheme_indices(true)
254
308
                        .next()
255
308
                        .map(|(_, grapheme)| cluster_start_byte + grapheme.len())
256
308
                        .unwrap_or(run.text.len())
257
                }
258
            },
259
        };
260
        
261
572
        if byte_offset <= run.text.len() {
262
572
            run.text.insert_str(byte_offset, text_to_insert);
263

            
264
572
            let new_cursor = TextCursor {
265
572
                cluster_id: GraphemeClusterId {
266
572
                    source_run: run_idx as u32,
267
572
                    start_byte_in_run: (byte_offset + text_to_insert.len()) as u32,
268
572
                },
269
572
                affinity: CursorAffinity::Leading,
270
572
            };
271
572
            return (new_content, new_cursor);
272
        }
273
    }
274

            
275
    // If insertion failed, return original state
276
    (content.to_vec(), *cursor)
277
572
}
278

            
279
/// Deletes one grapheme cluster backward from the cursor.
280
/// 
281
/// The cursor's affinity determines the actual cursor position:
282
/// - `Leading`: Cursor is at start of cluster, delete the previous grapheme
283
/// - `Trailing`: Cursor is at end of cluster, delete the current grapheme
284
pub fn delete_backward(
285
    content: &mut Vec<InlineContent>,
286
    cursor: &TextCursor,
287
) -> (Vec<InlineContent>, TextCursor) {
288
    use unicode_segmentation::UnicodeSegmentation;
289
    let mut new_content = content.clone();
290
    let run_idx = cursor.cluster_id.source_run as usize;
291
    let cluster_start_byte = cursor.cluster_id.start_byte_in_run as usize;
292

            
293
    if let Some(InlineContent::Text(run)) = new_content.get_mut(run_idx) {
294
        // Calculate the actual cursor byte offset based on affinity
295
        let byte_offset = match cursor.affinity {
296
            CursorAffinity::Leading => cluster_start_byte,
297
            CursorAffinity::Trailing => {
298
                // Cursor is at end of cluster - find the next grapheme boundary
299
                if cluster_start_byte >= run.text.len() {
300
                    run.text.len()
301
                } else {
302
                    run.text[cluster_start_byte..]
303
                        .grapheme_indices(true)
304
                        .next()
305
                        .map(|(_, grapheme)| cluster_start_byte + grapheme.len())
306
                        .unwrap_or(run.text.len())
307
                }
308
            },
309
        };
310
        
311
        if byte_offset > 0 {
312
            let prev_grapheme_start = run.text[..byte_offset]
313
                .grapheme_indices(true)
314
                .last()
315
                .map_or(0, |(i, _)| i);
316
            run.text.drain(prev_grapheme_start..byte_offset);
317

            
318
            let new_cursor = TextCursor {
319
                cluster_id: GraphemeClusterId {
320
                    source_run: run_idx as u32,
321
                    start_byte_in_run: prev_grapheme_start as u32,
322
                },
323
                affinity: CursorAffinity::Leading,
324
            };
325
            return (new_content, new_cursor);
326
        } else if run_idx > 0 {
327
            // Handle deleting across run boundaries (merge with previous run)
328
            if let Some(InlineContent::Text(prev_run)) = content.get(run_idx - 1).cloned() {
329
                let mut merged_text = prev_run.text;
330
                let new_cursor_byte_offset = merged_text.len();
331
                merged_text.push_str(&run.text);
332

            
333
                new_content[run_idx - 1] = InlineContent::Text(StyledRun {
334
                    text: merged_text,
335
                    style: prev_run.style,
336
                    logical_start_byte: prev_run.logical_start_byte,
337
                    source_node_id: prev_run.source_node_id,
338
                });
339
                new_content.remove(run_idx);
340

            
341
                let new_cursor = TextCursor {
342
                    cluster_id: GraphemeClusterId {
343
                        source_run: (run_idx - 1) as u32,
344
                        start_byte_in_run: new_cursor_byte_offset as u32,
345
                    },
346
                    affinity: CursorAffinity::Leading,
347
                };
348
                return (new_content, new_cursor);
349
            }
350
        }
351
    }
352

            
353
    (content.to_vec(), *cursor)
354
}
355

            
356
/// Deletes one grapheme cluster forward from the cursor.
357
/// 
358
/// The cursor's affinity determines the actual cursor position:
359
/// - `Leading`: Cursor is at start of cluster, delete the current grapheme
360
/// - `Trailing`: Cursor is at end of cluster, delete the next grapheme
361
pub fn delete_forward(
362
    content: &mut Vec<InlineContent>,
363
    cursor: &TextCursor,
364
) -> (Vec<InlineContent>, TextCursor) {
365
    use unicode_segmentation::UnicodeSegmentation;
366
    let mut new_content = content.clone();
367
    let run_idx = cursor.cluster_id.source_run as usize;
368
    let cluster_start_byte = cursor.cluster_id.start_byte_in_run as usize;
369

            
370
    if let Some(InlineContent::Text(run)) = new_content.get_mut(run_idx) {
371
        // Calculate the actual cursor byte offset based on affinity
372
        let byte_offset = match cursor.affinity {
373
            CursorAffinity::Leading => cluster_start_byte,
374
            CursorAffinity::Trailing => {
375
                // Cursor is at end of cluster - find the next grapheme boundary
376
                if cluster_start_byte >= run.text.len() {
377
                    run.text.len()
378
                } else {
379
                    run.text[cluster_start_byte..]
380
                        .grapheme_indices(true)
381
                        .next()
382
                        .map(|(_, grapheme)| cluster_start_byte + grapheme.len())
383
                        .unwrap_or(run.text.len())
384
                }
385
            },
386
        };
387
        
388
        if byte_offset < run.text.len() {
389
            let next_grapheme_end = run.text[byte_offset..]
390
                .grapheme_indices(true)
391
                .nth(1)
392
                .map_or(run.text.len(), |(i, _)| byte_offset + i);
393
            run.text.drain(byte_offset..next_grapheme_end);
394

            
395
            // Cursor position stays at the same byte offset but with Leading affinity
396
            let new_cursor = TextCursor {
397
                cluster_id: GraphemeClusterId {
398
                    source_run: run_idx as u32,
399
                    start_byte_in_run: byte_offset as u32,
400
                },
401
                affinity: CursorAffinity::Leading,
402
            };
403
            return (new_content, new_cursor);
404
        } else if run_idx < content.len() - 1 {
405
            // Handle deleting across run boundaries (merge with next run)
406
            if let Some(InlineContent::Text(next_run)) = content.get(run_idx + 1).cloned() {
407
                let mut merged_text = run.text.clone();
408
                merged_text.push_str(&next_run.text);
409

            
410
                new_content[run_idx] = InlineContent::Text(StyledRun {
411
                    text: merged_text,
412
                    style: run.style.clone(),
413
                    logical_start_byte: run.logical_start_byte,
414
                    source_node_id: run.source_node_id,
415
                });
416
                new_content.remove(run_idx + 1);
417

            
418
                return (new_content, *cursor);
419
            }
420
        }
421
    }
422

            
423
    (content.to_vec(), *cursor)
424
}
425

            
426
/// Edit text with different text per selection (for N-lines-to-N-cursors paste).
427
///
428
/// Each selection gets its own text inserted. Selections are processed back-to-front
429
/// to avoid index invalidation. Returns the new content and updated cursors.
430
///
431
/// # Panics
432
///
433
/// Panics if `texts.len() != selections.len()`.
434
pub fn edit_text_multi(
435
    content: &[InlineContent],
436
    selections: &[Selection],
437
    texts: &[&str],
438
) -> (Vec<InlineContent>, Vec<Selection>) {
439
    assert_eq!(
440
        selections.len(),
441
        texts.len(),
442
        "edit_text_multi: selections and texts must have the same length"
443
    );
444

            
445
    if selections.is_empty() {
446
        return (content.to_vec(), Vec::new());
447
    }
448

            
449
    let mut new_content = content.to_vec();
450
    let mut new_selections = Vec::new();
451

            
452
    // Pair selections with their text, sort back-to-front
453
    let mut pairs: Vec<(Selection, &str)> = selections
454
        .iter()
455
        .copied()
456
        .zip(texts.iter().copied())
457
        .collect();
458
    pairs.sort_by(|a, b| {
459
        let cursor_a = match &a.0 {
460
            Selection::Cursor(c) => c,
461
            Selection::Range(r) => &r.start,
462
        };
463
        let cursor_b = match &b.0 {
464
            Selection::Cursor(c) => c,
465
            Selection::Range(r) => &r.start,
466
        };
467
        cursor_b.cluster_id.cmp(&cursor_a.cluster_id) // Reverse sort
468
    });
469

            
470
    for (selection, text) in &pairs {
471
        let edit = TextEdit::Insert(text.to_string());
472
        let (temp_content, new_cursor) =
473
            apply_edit_to_selection(&new_content, selection, &edit);
474

            
475
        let edit_run = match selection {
476
            Selection::Cursor(c) => c.cluster_id.source_run,
477
            Selection::Range(r) => r.start.cluster_id.source_run,
478
        };
479
        let edit_byte = match selection {
480
            Selection::Cursor(c) => c.cluster_id.start_byte_in_run,
481
            Selection::Range(r) => r.start.cluster_id.start_byte_in_run,
482
        };
483

            
484
        let byte_offset_change = text.len() as i32;
485

            
486
        for prev_selection in new_selections.iter_mut() {
487
            if let Selection::Cursor(cursor) = prev_selection {
488
                if cursor.cluster_id.source_run == edit_run
489
                    && cursor.cluster_id.start_byte_in_run >= edit_byte
490
                {
491
                    cursor.cluster_id.start_byte_in_run =
492
                        (cursor.cluster_id.start_byte_in_run as i32 + byte_offset_change).max(0)
493
                            as u32;
494
                }
495
            }
496
        }
497

            
498
        new_content = temp_content;
499
        new_selections.push(Selection::Cursor(new_cursor));
500
    }
501

            
502
    new_selections.reverse();
503
    (new_content, new_selections)
504
}
505

            
506
/// Returns the range and text that a delete operation would remove, without
507
/// actually modifying the content. Useful for callbacks that need to inspect
508
/// pending deletes. Returns `None` if nothing would be deleted.
509
pub fn inspect_delete(
510
    content: &[InlineContent],
511
    selection: &Selection,
512
    forward: bool,
513
) -> Option<(SelectionRange, String)> {
514
    match selection {
515
        Selection::Range(range) => {
516
            // If there's already a selection, that's what would be deleted
517
            let deleted_text = extract_text_in_range(content, range);
518
            Some((*range, deleted_text))
519
        }
520
        Selection::Cursor(cursor) => {
521
            // No selection - would delete one grapheme cluster
522
            if forward {
523
                inspect_delete_forward(content, cursor)
524
            } else {
525
                inspect_delete_backward(content, cursor)
526
            }
527
        }
528
    }
529
}
530

            
531
/// Inspect what would be deleted by delete-forward (Delete key)
532
fn inspect_delete_forward(
533
    content: &[InlineContent],
534
    cursor: &TextCursor,
535
) -> Option<(SelectionRange, String)> {
536
    use unicode_segmentation::UnicodeSegmentation;
537

            
538
    let run_idx = cursor.cluster_id.source_run as usize;
539
    let byte_offset = cursor.cluster_id.start_byte_in_run as usize;
540

            
541
    if let Some(InlineContent::Text(run)) = content.get(run_idx) {
542
        if byte_offset < run.text.len() {
543
            // Delete within same run
544
            let next_grapheme_end = run.text[byte_offset..]
545
                .grapheme_indices(true)
546
                .nth(1)
547
                .map_or(run.text.len(), |(i, _)| byte_offset + i);
548

            
549
            let deleted_text = run.text[byte_offset..next_grapheme_end].to_string();
550

            
551
            let range = SelectionRange {
552
                start: *cursor,
553
                end: TextCursor {
554
                    cluster_id: GraphemeClusterId {
555
                        source_run: run_idx as u32,
556
                        start_byte_in_run: next_grapheme_end as u32,
557
                    },
558
                    affinity: CursorAffinity::Leading,
559
                },
560
            };
561

            
562
            return Some((range, deleted_text));
563
        } else if run_idx < content.len() - 1 {
564
            // Would delete across run boundary
565
            if let Some(InlineContent::Text(next_run)) = content.get(run_idx + 1) {
566
                let deleted_text = next_run.text.graphemes(true).next()?.to_string();
567

            
568
                let next_grapheme_end = next_run
569
                    .text
570
                    .grapheme_indices(true)
571
                    .nth(1)
572
                    .map_or(next_run.text.len(), |(i, _)| i);
573

            
574
                let range = SelectionRange {
575
                    start: *cursor,
576
                    end: TextCursor {
577
                        cluster_id: GraphemeClusterId {
578
                            source_run: (run_idx + 1) as u32,
579
                            start_byte_in_run: next_grapheme_end as u32,
580
                        },
581
                        affinity: CursorAffinity::Leading,
582
                    },
583
                };
584

            
585
                return Some((range, deleted_text));
586
            }
587
        }
588
    }
589

            
590
    None // At end of document, nothing to delete
591
}
592

            
593
/// Inspect what would be deleted by delete-backward (Backspace key)
594
fn inspect_delete_backward(
595
    content: &[InlineContent],
596
    cursor: &TextCursor,
597
) -> Option<(SelectionRange, String)> {
598
    use unicode_segmentation::UnicodeSegmentation;
599

            
600
    let run_idx = cursor.cluster_id.source_run as usize;
601
    let byte_offset = cursor.cluster_id.start_byte_in_run as usize;
602

            
603
    if let Some(InlineContent::Text(run)) = content.get(run_idx) {
604
        if byte_offset > 0 {
605
            // Delete within same run
606
            let prev_grapheme_start = run.text[..byte_offset]
607
                .grapheme_indices(true)
608
                .last()
609
                .map_or(0, |(i, _)| i);
610

            
611
            let deleted_text = run.text[prev_grapheme_start..byte_offset].to_string();
612

            
613
            let range = SelectionRange {
614
                start: TextCursor {
615
                    cluster_id: GraphemeClusterId {
616
                        source_run: run_idx as u32,
617
                        start_byte_in_run: prev_grapheme_start as u32,
618
                    },
619
                    affinity: CursorAffinity::Leading,
620
                },
621
                end: *cursor,
622
            };
623

            
624
            return Some((range, deleted_text));
625
        } else if run_idx > 0 {
626
            // Would delete across run boundary
627
            if let Some(InlineContent::Text(prev_run)) = content.get(run_idx - 1) {
628
                let deleted_text = prev_run.text.graphemes(true).last()?.to_string();
629

            
630
                let prev_grapheme_start = prev_run.text[..]
631
                    .grapheme_indices(true)
632
                    .last()
633
                    .map_or(0, |(i, _)| i);
634

            
635
                let range = SelectionRange {
636
                    start: TextCursor {
637
                        cluster_id: GraphemeClusterId {
638
                            source_run: (run_idx - 1) as u32,
639
                            start_byte_in_run: prev_grapheme_start as u32,
640
                        },
641
                        affinity: CursorAffinity::Leading,
642
                    },
643
                    end: *cursor,
644
                };
645

            
646
                return Some((range, deleted_text));
647
            }
648
        }
649
    }
650

            
651
    None // At start of document, nothing to delete
652
}
653

            
654
/// Extract the text within a selection range
655
fn extract_text_in_range(content: &[InlineContent], range: &SelectionRange) -> String {
656
    let start_run = range.start.cluster_id.source_run as usize;
657
    let end_run = range.end.cluster_id.source_run as usize;
658
    let start_byte = range.start.cluster_id.start_byte_in_run as usize;
659
    let end_byte = range.end.cluster_id.start_byte_in_run as usize;
660

            
661
    if start_run == end_run {
662
        // Single run
663
        if let Some(InlineContent::Text(run)) = content.get(start_run) {
664
            if start_byte <= end_byte && end_byte <= run.text.len() {
665
                return run.text[start_byte..end_byte].to_string();
666
            }
667
        }
668
    } else {
669
        // Multi-run selection (simplified - full implementation would handle images, etc.)
670
        let mut result = String::new();
671

            
672
        for (idx, item) in content.iter().enumerate() {
673
            if let InlineContent::Text(run) = item {
674
                if idx == start_run {
675
                    // First run - from start_byte to end
676
                    if start_byte < run.text.len() {
677
                        result.push_str(&run.text[start_byte..]);
678
                    }
679
                } else if idx > start_run && idx < end_run {
680
                    // Middle runs - entire text
681
                    result.push_str(&run.text);
682
                } else if idx == end_run {
683
                    // Last run - from 0 to end_byte
684
                    if end_byte <= run.text.len() {
685
                        result.push_str(&run.text[..end_byte]);
686
                    }
687
                    break;
688
                }
689
            }
690
        }
691

            
692
        return result;
693
    }
694

            
695
    String::new()
696
}