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
455
pub fn edit_text(
29
455
    content: &[InlineContent],
30
455
    selections: &[Selection],
31
455
    edit: &TextEdit,
32
455
) -> (Vec<InlineContent>, Vec<Selection>) {
33
455
    if selections.is_empty() {
34
        return (content.to_vec(), Vec::new());
35
455
    }
36

            
37
455
    let mut new_content = content.to_vec();
38
455
    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
455
    let mut sorted_selections = selections.to_vec();
44
455
    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
910
    for selection in sorted_selections {
57
455
        let (mut temp_content, new_cursor) =
58
455
            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
455
        let edit_run = match selection {
63
455
            Selection::Cursor(c) => c.cluster_id.source_run,
64
            Selection::Range(r) => r.start.cluster_id.source_run,
65
        };
66
455
        let edit_byte = match selection {
67
455
            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
455
        let byte_offset_change: i32 = match edit {
73
455
            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
455
        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
455
        new_content = temp_content;
95
455
        new_selections.push(Selection::Cursor(new_cursor));
96
    }
97

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

            
101
455
    (new_content, new_selections)
102
455
}
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
455
pub fn apply_edit_to_selection(
112
455
    content: &[InlineContent],
113
455
    selection: &Selection,
114
455
    edit: &TextEdit,
115
455
) -> (Vec<InlineContent>, TextCursor) {
116
455
    let mut new_content = content.to_vec();
117

            
118
455
    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
455
        Selection::Cursor(cursor) => {
135
455
            match edit {
136
455
                TextEdit::Insert(text_to_insert) => {
137
455
                    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
455
}
145

            
146
/// Deletes the content within a given range.
147
pub fn delete_range(
148
    content: &[InlineContent],
149
    range: &SelectionRange,
150
) -> (Vec<InlineContent>, TextCursor) {
151
    // This is a highly complex function. A full implementation needs to handle:
152
    //
153
    // - Deletions within a single text run.
154
    // - Deletions that span across multiple text runs.
155
    // - Deletions that include non-text items like images.
156
    //
157
    // For now, we provide a simplified version that handles deletion within a
158
    // single run.
159

            
160
    let mut new_content = content.to_vec();
161
    let start_run_idx = range.start.cluster_id.source_run as usize;
162
    let end_run_idx = range.end.cluster_id.source_run as usize;
163

            
164
    if start_run_idx == end_run_idx {
165
        if let Some(InlineContent::Text(run)) = new_content.get_mut(start_run_idx) {
166
            let start_byte = range.start.cluster_id.start_byte_in_run as usize;
167
            let end_byte = range.end.cluster_id.start_byte_in_run as usize;
168
            if start_byte <= end_byte && end_byte <= run.text.len() {
169
                run.text.drain(start_byte..end_byte);
170
            }
171
        }
172
    } else {
173
        // TODO: Handle multi-run deletion
174
    }
175

            
176
    (new_content, range.start) // Return cursor at the start of the deleted range
177
}
178

            
179
/// Inserts text at a cursor position.
180
/// 
181
/// The cursor's affinity determines the exact insertion point:
182
/// - `Leading`: Insert at the start of the referenced cluster (start_byte_in_run)
183
/// - `Trailing`: Insert at the end of the referenced cluster (after the grapheme)
184
455
pub fn insert_text(
185
455
    content: &mut Vec<InlineContent>,
186
455
    cursor: &TextCursor,
187
455
    text_to_insert: &str,
188
455
) -> (Vec<InlineContent>, TextCursor) {
189
    use unicode_segmentation::UnicodeSegmentation;
190
    
191
455
    let mut new_content = content.clone();
192
455
    let run_idx = cursor.cluster_id.source_run as usize;
193
455
    let cluster_start_byte = cursor.cluster_id.start_byte_in_run as usize;
194

            
195
455
    if let Some(InlineContent::Text(run)) = new_content.get_mut(run_idx) {
196
        // Calculate the actual insertion byte offset based on affinity
197
455
        let byte_offset = match cursor.affinity {
198
            CursorAffinity::Leading => {
199
                // Insert at the start of the cluster
200
210
                cluster_start_byte
201
            },
202
            CursorAffinity::Trailing => {
203
                // Insert at the end of the cluster - find the next grapheme boundary
204
                // We need to find where this grapheme cluster ends
205
245
                if cluster_start_byte >= run.text.len() {
206
                    // Cursor is at/past end of run - insert at end
207
                    run.text.len()
208
                } else {
209
                    // Find the grapheme that starts at cluster_start_byte and get its end
210
245
                    run.text[cluster_start_byte..]
211
245
                        .grapheme_indices(true)
212
245
                        .next()
213
245
                        .map(|(_, grapheme)| cluster_start_byte + grapheme.len())
214
245
                        .unwrap_or(run.text.len())
215
                }
216
            },
217
        };
218
        
219
455
        if byte_offset <= run.text.len() {
220
455
            run.text.insert_str(byte_offset, text_to_insert);
221

            
222
455
            let new_cursor = TextCursor {
223
455
                cluster_id: GraphemeClusterId {
224
455
                    source_run: run_idx as u32,
225
455
                    start_byte_in_run: (byte_offset + text_to_insert.len()) as u32,
226
455
                },
227
455
                affinity: CursorAffinity::Leading,
228
455
            };
229
455
            return (new_content, new_cursor);
230
        }
231
    }
232

            
233
    // If insertion failed, return original state
234
    (content.to_vec(), *cursor)
235
455
}
236

            
237
/// Deletes one grapheme cluster backward from the cursor.
238
/// 
239
/// The cursor's affinity determines the actual cursor position:
240
/// - `Leading`: Cursor is at start of cluster, delete the previous grapheme
241
/// - `Trailing`: Cursor is at end of cluster, delete the current grapheme
242
pub fn delete_backward(
243
    content: &mut Vec<InlineContent>,
244
    cursor: &TextCursor,
245
) -> (Vec<InlineContent>, TextCursor) {
246
    use unicode_segmentation::UnicodeSegmentation;
247
    let mut new_content = content.clone();
248
    let run_idx = cursor.cluster_id.source_run as usize;
249
    let cluster_start_byte = cursor.cluster_id.start_byte_in_run as usize;
250

            
251
    if let Some(InlineContent::Text(run)) = new_content.get_mut(run_idx) {
252
        // Calculate the actual cursor byte offset based on affinity
253
        let byte_offset = match cursor.affinity {
254
            CursorAffinity::Leading => cluster_start_byte,
255
            CursorAffinity::Trailing => {
256
                // Cursor is at end of cluster - find the next grapheme boundary
257
                if cluster_start_byte >= run.text.len() {
258
                    run.text.len()
259
                } else {
260
                    run.text[cluster_start_byte..]
261
                        .grapheme_indices(true)
262
                        .next()
263
                        .map(|(_, grapheme)| cluster_start_byte + grapheme.len())
264
                        .unwrap_or(run.text.len())
265
                }
266
            },
267
        };
268
        
269
        if byte_offset > 0 {
270
            let prev_grapheme_start = run.text[..byte_offset]
271
                .grapheme_indices(true)
272
                .last()
273
                .map_or(0, |(i, _)| i);
274
            run.text.drain(prev_grapheme_start..byte_offset);
275

            
276
            let new_cursor = TextCursor {
277
                cluster_id: GraphemeClusterId {
278
                    source_run: run_idx as u32,
279
                    start_byte_in_run: prev_grapheme_start as u32,
280
                },
281
                affinity: CursorAffinity::Leading,
282
            };
283
            return (new_content, new_cursor);
284
        } else if run_idx > 0 {
285
            // Handle deleting across run boundaries (merge with previous run)
286
            if let Some(InlineContent::Text(prev_run)) = content.get(run_idx - 1).cloned() {
287
                let mut merged_text = prev_run.text;
288
                let new_cursor_byte_offset = merged_text.len();
289
                merged_text.push_str(&run.text);
290

            
291
                new_content[run_idx - 1] = InlineContent::Text(StyledRun {
292
                    text: merged_text,
293
                    style: prev_run.style,
294
                    logical_start_byte: prev_run.logical_start_byte,
295
                    source_node_id: prev_run.source_node_id,
296
                });
297
                new_content.remove(run_idx);
298

            
299
                let new_cursor = TextCursor {
300
                    cluster_id: GraphemeClusterId {
301
                        source_run: (run_idx - 1) as u32,
302
                        start_byte_in_run: new_cursor_byte_offset as u32,
303
                    },
304
                    affinity: CursorAffinity::Leading,
305
                };
306
                return (new_content, new_cursor);
307
            }
308
        }
309
    }
310

            
311
    (content.to_vec(), *cursor)
312
}
313

            
314
/// Deletes one grapheme cluster forward from the cursor.
315
/// 
316
/// The cursor's affinity determines the actual cursor position:
317
/// - `Leading`: Cursor is at start of cluster, delete the current grapheme
318
/// - `Trailing`: Cursor is at end of cluster, delete the next grapheme
319
pub fn delete_forward(
320
    content: &mut Vec<InlineContent>,
321
    cursor: &TextCursor,
322
) -> (Vec<InlineContent>, TextCursor) {
323
    use unicode_segmentation::UnicodeSegmentation;
324
    let mut new_content = content.clone();
325
    let run_idx = cursor.cluster_id.source_run as usize;
326
    let cluster_start_byte = cursor.cluster_id.start_byte_in_run as usize;
327

            
328
    if let Some(InlineContent::Text(run)) = new_content.get_mut(run_idx) {
329
        // Calculate the actual cursor byte offset based on affinity
330
        let byte_offset = match cursor.affinity {
331
            CursorAffinity::Leading => cluster_start_byte,
332
            CursorAffinity::Trailing => {
333
                // Cursor is at end of cluster - find the next grapheme boundary
334
                if cluster_start_byte >= run.text.len() {
335
                    run.text.len()
336
                } else {
337
                    run.text[cluster_start_byte..]
338
                        .grapheme_indices(true)
339
                        .next()
340
                        .map(|(_, grapheme)| cluster_start_byte + grapheme.len())
341
                        .unwrap_or(run.text.len())
342
                }
343
            },
344
        };
345
        
346
        if byte_offset < run.text.len() {
347
            let next_grapheme_end = run.text[byte_offset..]
348
                .grapheme_indices(true)
349
                .nth(1)
350
                .map_or(run.text.len(), |(i, _)| byte_offset + i);
351
            run.text.drain(byte_offset..next_grapheme_end);
352

            
353
            // Cursor position stays at the same byte offset but with Leading affinity
354
            let new_cursor = TextCursor {
355
                cluster_id: GraphemeClusterId {
356
                    source_run: run_idx as u32,
357
                    start_byte_in_run: byte_offset as u32,
358
                },
359
                affinity: CursorAffinity::Leading,
360
            };
361
            return (new_content, new_cursor);
362
        } else if run_idx < content.len() - 1 {
363
            // Handle deleting across run boundaries (merge with next run)
364
            if let Some(InlineContent::Text(next_run)) = content.get(run_idx + 1).cloned() {
365
                let mut merged_text = run.text.clone();
366
                merged_text.push_str(&next_run.text);
367

            
368
                new_content[run_idx] = InlineContent::Text(StyledRun {
369
                    text: merged_text,
370
                    style: run.style.clone(),
371
                    logical_start_byte: run.logical_start_byte,
372
                    source_node_id: run.source_node_id,
373
                });
374
                new_content.remove(run_idx + 1);
375

            
376
                return (new_content, *cursor);
377
            }
378
        }
379
    }
380

            
381
    (content.to_vec(), *cursor)
382
}
383

            
384
/// Edit text with different text per selection (for N-lines-to-N-cursors paste).
385
///
386
/// Each selection gets its own text inserted. Selections are processed back-to-front
387
/// to avoid index invalidation. Returns the new content and updated cursors.
388
///
389
/// # Panics
390
///
391
/// Panics if `texts.len() != selections.len()`.
392
pub fn edit_text_multi(
393
    content: &[InlineContent],
394
    selections: &[Selection],
395
    texts: &[&str],
396
) -> (Vec<InlineContent>, Vec<Selection>) {
397
    assert_eq!(
398
        selections.len(),
399
        texts.len(),
400
        "edit_text_multi: selections and texts must have the same length"
401
    );
402

            
403
    if selections.is_empty() {
404
        return (content.to_vec(), Vec::new());
405
    }
406

            
407
    let mut new_content = content.to_vec();
408
    let mut new_selections = Vec::new();
409

            
410
    // Pair selections with their text, sort back-to-front
411
    let mut pairs: Vec<(Selection, &str)> = selections
412
        .iter()
413
        .copied()
414
        .zip(texts.iter().copied())
415
        .collect();
416
    pairs.sort_by(|a, b| {
417
        let cursor_a = match &a.0 {
418
            Selection::Cursor(c) => c,
419
            Selection::Range(r) => &r.start,
420
        };
421
        let cursor_b = match &b.0 {
422
            Selection::Cursor(c) => c,
423
            Selection::Range(r) => &r.start,
424
        };
425
        cursor_b.cluster_id.cmp(&cursor_a.cluster_id) // Reverse sort
426
    });
427

            
428
    for (selection, text) in &pairs {
429
        let edit = TextEdit::Insert(text.to_string());
430
        let (temp_content, new_cursor) =
431
            apply_edit_to_selection(&new_content, selection, &edit);
432

            
433
        let edit_run = match selection {
434
            Selection::Cursor(c) => c.cluster_id.source_run,
435
            Selection::Range(r) => r.start.cluster_id.source_run,
436
        };
437
        let edit_byte = match selection {
438
            Selection::Cursor(c) => c.cluster_id.start_byte_in_run,
439
            Selection::Range(r) => r.start.cluster_id.start_byte_in_run,
440
        };
441

            
442
        let byte_offset_change = text.len() as i32;
443

            
444
        for prev_selection in new_selections.iter_mut() {
445
            if let Selection::Cursor(cursor) = prev_selection {
446
                if cursor.cluster_id.source_run == edit_run
447
                    && cursor.cluster_id.start_byte_in_run >= edit_byte
448
                {
449
                    cursor.cluster_id.start_byte_in_run =
450
                        (cursor.cluster_id.start_byte_in_run as i32 + byte_offset_change).max(0)
451
                            as u32;
452
                }
453
            }
454
        }
455

            
456
        new_content = temp_content;
457
        new_selections.push(Selection::Cursor(new_cursor));
458
    }
459

            
460
    new_selections.reverse();
461
    (new_content, new_selections)
462
}
463

            
464
/// Returns the range and text that a delete operation would remove, without
465
/// actually modifying the content. Useful for callbacks that need to inspect
466
/// pending deletes. Returns `None` if nothing would be deleted.
467
pub fn inspect_delete(
468
    content: &[InlineContent],
469
    selection: &Selection,
470
    forward: bool,
471
) -> Option<(SelectionRange, String)> {
472
    match selection {
473
        Selection::Range(range) => {
474
            // If there's already a selection, that's what would be deleted
475
            let deleted_text = extract_text_in_range(content, range);
476
            Some((*range, deleted_text))
477
        }
478
        Selection::Cursor(cursor) => {
479
            // No selection - would delete one grapheme cluster
480
            if forward {
481
                inspect_delete_forward(content, cursor)
482
            } else {
483
                inspect_delete_backward(content, cursor)
484
            }
485
        }
486
    }
487
}
488

            
489
/// Inspect what would be deleted by delete-forward (Delete key)
490
fn inspect_delete_forward(
491
    content: &[InlineContent],
492
    cursor: &TextCursor,
493
) -> Option<(SelectionRange, String)> {
494
    use unicode_segmentation::UnicodeSegmentation;
495

            
496
    let run_idx = cursor.cluster_id.source_run as usize;
497
    let byte_offset = cursor.cluster_id.start_byte_in_run as usize;
498

            
499
    if let Some(InlineContent::Text(run)) = content.get(run_idx) {
500
        if byte_offset < run.text.len() {
501
            // Delete within same run
502
            let next_grapheme_end = run.text[byte_offset..]
503
                .grapheme_indices(true)
504
                .nth(1)
505
                .map_or(run.text.len(), |(i, _)| byte_offset + i);
506

            
507
            let deleted_text = run.text[byte_offset..next_grapheme_end].to_string();
508

            
509
            let range = SelectionRange {
510
                start: *cursor,
511
                end: TextCursor {
512
                    cluster_id: GraphemeClusterId {
513
                        source_run: run_idx as u32,
514
                        start_byte_in_run: next_grapheme_end as u32,
515
                    },
516
                    affinity: CursorAffinity::Leading,
517
                },
518
            };
519

            
520
            return Some((range, deleted_text));
521
        } else if run_idx < content.len() - 1 {
522
            // Would delete across run boundary
523
            if let Some(InlineContent::Text(next_run)) = content.get(run_idx + 1) {
524
                let deleted_text = next_run.text.graphemes(true).next()?.to_string();
525

            
526
                let next_grapheme_end = next_run
527
                    .text
528
                    .grapheme_indices(true)
529
                    .nth(1)
530
                    .map_or(next_run.text.len(), |(i, _)| i);
531

            
532
                let range = SelectionRange {
533
                    start: *cursor,
534
                    end: TextCursor {
535
                        cluster_id: GraphemeClusterId {
536
                            source_run: (run_idx + 1) as u32,
537
                            start_byte_in_run: next_grapheme_end as u32,
538
                        },
539
                        affinity: CursorAffinity::Leading,
540
                    },
541
                };
542

            
543
                return Some((range, deleted_text));
544
            }
545
        }
546
    }
547

            
548
    None // At end of document, nothing to delete
549
}
550

            
551
/// Inspect what would be deleted by delete-backward (Backspace key)
552
fn inspect_delete_backward(
553
    content: &[InlineContent],
554
    cursor: &TextCursor,
555
) -> Option<(SelectionRange, String)> {
556
    use unicode_segmentation::UnicodeSegmentation;
557

            
558
    let run_idx = cursor.cluster_id.source_run as usize;
559
    let byte_offset = cursor.cluster_id.start_byte_in_run as usize;
560

            
561
    if let Some(InlineContent::Text(run)) = content.get(run_idx) {
562
        if byte_offset > 0 {
563
            // Delete within same run
564
            let prev_grapheme_start = run.text[..byte_offset]
565
                .grapheme_indices(true)
566
                .last()
567
                .map_or(0, |(i, _)| i);
568

            
569
            let deleted_text = run.text[prev_grapheme_start..byte_offset].to_string();
570

            
571
            let range = SelectionRange {
572
                start: TextCursor {
573
                    cluster_id: GraphemeClusterId {
574
                        source_run: run_idx as u32,
575
                        start_byte_in_run: prev_grapheme_start as u32,
576
                    },
577
                    affinity: CursorAffinity::Leading,
578
                },
579
                end: *cursor,
580
            };
581

            
582
            return Some((range, deleted_text));
583
        } else if run_idx > 0 {
584
            // Would delete across run boundary
585
            if let Some(InlineContent::Text(prev_run)) = content.get(run_idx - 1) {
586
                let deleted_text = prev_run.text.graphemes(true).last()?.to_string();
587

            
588
                let prev_grapheme_start = prev_run.text[..]
589
                    .grapheme_indices(true)
590
                    .last()
591
                    .map_or(0, |(i, _)| i);
592

            
593
                let range = SelectionRange {
594
                    start: TextCursor {
595
                        cluster_id: GraphemeClusterId {
596
                            source_run: (run_idx - 1) as u32,
597
                            start_byte_in_run: prev_grapheme_start as u32,
598
                        },
599
                        affinity: CursorAffinity::Leading,
600
                    },
601
                    end: *cursor,
602
                };
603

            
604
                return Some((range, deleted_text));
605
            }
606
        }
607
    }
608

            
609
    None // At start of document, nothing to delete
610
}
611

            
612
/// Extract the text within a selection range
613
fn extract_text_in_range(content: &[InlineContent], range: &SelectionRange) -> String {
614
    let start_run = range.start.cluster_id.source_run as usize;
615
    let end_run = range.end.cluster_id.source_run as usize;
616
    let start_byte = range.start.cluster_id.start_byte_in_run as usize;
617
    let end_byte = range.end.cluster_id.start_byte_in_run as usize;
618

            
619
    if start_run == end_run {
620
        // Single run
621
        if let Some(InlineContent::Text(run)) = content.get(start_run) {
622
            if start_byte <= end_byte && end_byte <= run.text.len() {
623
                return run.text[start_byte..end_byte].to_string();
624
            }
625
        }
626
    } else {
627
        // Multi-run selection (simplified - full implementation would handle images, etc.)
628
        let mut result = String::new();
629

            
630
        for (idx, item) in content.iter().enumerate() {
631
            if let InlineContent::Text(run) = item {
632
                if idx == start_run {
633
                    // First run - from start_byte to end
634
                    if start_byte < run.text.len() {
635
                        result.push_str(&run.text[start_byte..]);
636
                    }
637
                } else if idx > start_run && idx < end_run {
638
                    // Middle runs - entire text
639
                    result.push_str(&run.text);
640
                } else if idx == end_run {
641
                    // Last run - from 0 to end_byte
642
                    if end_byte <= run.text.len() {
643
                        result.push_str(&run.text[..end_byte]);
644
                    }
645
                    break;
646
                }
647
            }
648
        }
649

            
650
        return result;
651
    }
652

            
653
    String::new()
654
}