Skip to main content

gpui_engine/
line_wrapper.rs

1use crate::{FontId, TextRun, TextSystem};
2use gpui_shared_string::SharedString;
3use gpui_types::{Pixels, px};
4use std::{
5    borrow::Cow,
6    collections::HashMap,
7    iter,
8    ops::{Deref, DerefMut},
9    sync::Arc,
10};
11
12/// Determines whether to truncate text from the start or end.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub enum TruncateFrom {
15    /// Truncate text from the start.
16    Start,
17    /// Truncate text from the end.
18    End,
19    /// Truncate text from the middle, preserving the start and end.
20    Middle,
21}
22
23/// The GPUI line wrapper, used to wrap lines of text to a given width.
24pub struct LineWrapper {
25    text_system: Arc<dyn TextSystem>,
26    pub(crate) font_id: FontId,
27    pub(crate) font_size: Pixels,
28    cached_ascii_char_widths: [Option<Pixels>; 128],
29    cached_other_char_widths: HashMap<char, Pixels>,
30}
31
32impl LineWrapper {
33    /// The maximum indent that can be applied to a line.
34    pub const MAX_INDENT: u32 = 256;
35
36    /// Creates a wrapper that measures and truncates text through `text_system`.
37    pub fn new(font_id: FontId, font_size: Pixels, text_system: Arc<dyn TextSystem>) -> Self {
38        Self {
39            text_system,
40            font_id,
41            font_size,
42            cached_ascii_char_widths: [None; 128],
43            cached_other_char_widths: HashMap::default(),
44        }
45    }
46
47    /// Wrap a line of text to the given width with this wrapper's font and font size.
48    pub fn wrap_line<'a>(
49        &'a mut self,
50        fragments: &'a [LineFragment],
51        wrap_width: Pixels,
52    ) -> impl Iterator<Item = Boundary> + 'a {
53        let mut width = px(0.);
54        let mut first_non_whitespace_ix = None;
55        let mut indent = None;
56        let mut last_candidate_ix = 0;
57        let mut last_candidate_width = px(0.);
58        let mut last_wrap_ix = 0;
59        let mut prev_c = '\0';
60        let mut index = 0;
61        let mut candidates = fragments
62            .iter()
63            .flat_map(move |fragment| fragment.wrap_boundary_candidates())
64            .peekable();
65        iter::from_fn(move || {
66            for candidate in candidates.by_ref() {
67                let ix = index;
68                index += candidate.len_utf8();
69                let mut new_prev_c = prev_c;
70                let item_width = match candidate {
71                    WrapBoundaryCandidate::Char { character: c } => {
72                        if c == '\n' {
73                            continue;
74                        }
75
76                        if Self::is_word_char(c) {
77                            if prev_c == ' ' && c != ' ' && first_non_whitespace_ix.is_some() {
78                                last_candidate_ix = ix;
79                                last_candidate_width = width;
80                            }
81                        } else {
82                            // CJK may not be space separated, e.g.: `Hello world你好世界`
83                            if c != ' ' && first_non_whitespace_ix.is_some() {
84                                last_candidate_ix = ix;
85                                last_candidate_width = width;
86                            }
87                        }
88
89                        if c != ' ' && first_non_whitespace_ix.is_none() {
90                            first_non_whitespace_ix = Some(ix);
91                        }
92
93                        new_prev_c = c;
94
95                        self.width_for_char(c)
96                    }
97                    WrapBoundaryCandidate::Element {
98                        width: element_width,
99                        ..
100                    } => {
101                        if prev_c == ' ' && first_non_whitespace_ix.is_some() {
102                            last_candidate_ix = ix;
103                            last_candidate_width = width;
104                        }
105
106                        if first_non_whitespace_ix.is_none() {
107                            first_non_whitespace_ix = Some(ix);
108                        }
109
110                        element_width
111                    }
112                };
113
114                width += item_width;
115                if width > wrap_width && ix > last_wrap_ix {
116                    if let (None, Some(first_non_whitespace_ix)) = (indent, first_non_whitespace_ix)
117                    {
118                        indent = Some(
119                            Self::MAX_INDENT.min((first_non_whitespace_ix - last_wrap_ix) as u32),
120                        );
121                    }
122
123                    if last_candidate_ix > 0 {
124                        last_wrap_ix = last_candidate_ix;
125                        width -= last_candidate_width;
126                        last_candidate_ix = 0;
127                    } else {
128                        last_wrap_ix = ix;
129                        width = item_width;
130                    }
131
132                    if let Some(indent) = indent {
133                        width += self.width_for_char(' ') * indent as f32;
134                    }
135
136                    return Some(Boundary::new(last_wrap_ix, indent.unwrap_or(0)));
137                }
138
139                prev_c = new_prev_c;
140            }
141
142            None
143        })
144    }
145
146    /// Determines if a line should be truncated based on its width.
147    ///
148    /// Returns the truncation index in `line`.
149    pub fn should_truncate_line(
150        &mut self,
151        line: &str,
152        truncate_width: Pixels,
153        truncation_affix: &str,
154        truncate_from: TruncateFrom,
155    ) -> Option<usize> {
156        let mut width = px(0.);
157        let suffix_width = truncation_affix
158            .chars()
159            .map(|c| self.width_for_char(c))
160            .fold(px(0.0), |a, x| a + x);
161        let mut truncate_ix = 0;
162
163        match truncate_from {
164            TruncateFrom::Start => {
165                for (ix, c) in line.char_indices().rev() {
166                    if width + suffix_width < truncate_width {
167                        truncate_ix = ix;
168                    }
169
170                    let char_width = self.width_for_char(c);
171                    width += char_width;
172
173                    if width.floor() > truncate_width {
174                        return Some(truncate_ix);
175                    }
176                }
177            }
178            TruncateFrom::End => {
179                for (ix, c) in line.char_indices() {
180                    if width + suffix_width < truncate_width {
181                        truncate_ix = ix;
182                    }
183
184                    let char_width = self.width_for_char(c);
185                    width += char_width;
186
187                    if width.floor() > truncate_width {
188                        return Some(truncate_ix);
189                    }
190                }
191            }
192            TruncateFrom::Middle => {}
193        }
194
195        None
196    }
197
198    fn should_truncate_line_middle(
199        &mut self,
200        line: &str,
201        truncate_width: Pixels,
202        truncation_affix: &str,
203    ) -> Option<(usize, usize)> {
204        let suffix_width = truncation_affix
205            .chars()
206            .map(|c| self.width_for_char(c))
207            .fold(px(0.0), |a, x| a + x);
208
209        let total_width: Pixels = line
210            .chars()
211            .map(|c| self.width_for_char(c))
212            .fold(px(0.0), |a, x| a + x);
213
214        if total_width <= truncate_width {
215            return None;
216        }
217
218        let content_budget = truncate_width - suffix_width;
219        if content_budget <= px(0.) {
220            return Some((0, line.len()));
221        }
222
223        let front_budget = content_budget * (2.0 / 3.0);
224        let back_budget = content_budget - front_budget;
225
226        let mut front_width = px(0.);
227        let mut front_end_ix = 0usize;
228        for (ix, c) in line.char_indices() {
229            let char_width = self.width_for_char(c);
230            if front_width + char_width > front_budget {
231                break;
232            }
233            front_width += char_width;
234            front_end_ix = ix + c.len_utf8();
235        }
236
237        let mut back_width = px(0.);
238        let mut back_start_ix = line.len();
239        for (ix, c) in line.char_indices().rev() {
240            let char_width = self.width_for_char(c);
241            if back_width + char_width > back_budget {
242                break;
243            }
244            back_width += char_width;
245            back_start_ix = ix;
246        }
247
248        if front_end_ix >= back_start_ix {
249            return Some((0, line.len()));
250        }
251
252        Some((front_end_ix, back_start_ix))
253    }
254
255    /// Truncate a line of text to the given width with this wrapper's font and font size.
256    pub fn truncate_line<'a>(
257        &mut self,
258        line: SharedString,
259        truncate_width: Pixels,
260        truncation_affix: &str,
261        runs: &'a [TextRun],
262        truncate_from: TruncateFrom,
263    ) -> (SharedString, Cow<'a, [TextRun]>) {
264        if truncate_from == TruncateFrom::Middle {
265            if let Some((front_end_ix, back_start_ix)) =
266                self.should_truncate_line_middle(&line, truncate_width, truncation_affix)
267            {
268                let result = SharedString::from(format!(
269                    "{}{truncation_affix}{}",
270                    &line[..front_end_ix],
271                    &line[back_start_ix..]
272                ));
273                let mut runs = runs.to_vec();
274                update_runs_after_middle_truncation(
275                    truncation_affix,
276                    &mut runs,
277                    front_end_ix,
278                    back_start_ix,
279                );
280                return (result, Cow::Owned(runs));
281            } else {
282                return (line, Cow::Borrowed(runs));
283            }
284        }
285
286        if let Some(truncate_ix) =
287            self.should_truncate_line(&line, truncate_width, truncation_affix, truncate_from)
288        {
289            let result = match truncate_from {
290                TruncateFrom::Start => SharedString::from(format!(
291                    "{truncation_affix}{}",
292                    &line[line.ceil_char_boundary(truncate_ix + 1)..]
293                )),
294                TruncateFrom::End => SharedString::from(format!(
295                    "{}{truncation_affix}",
296                    line[..truncate_ix]
297                        .trim_end_matches(|c: char| c.is_whitespace() || c.is_ascii_punctuation())
298                )),
299                TruncateFrom::Middle => unreachable!("Middle truncation is handled above"),
300            };
301            let mut runs = runs.to_vec();
302            update_runs_after_truncation(&result, truncation_affix, &mut runs, truncate_from);
303            (result, Cow::Owned(runs))
304        } else {
305            (line, Cow::Borrowed(runs))
306        }
307    }
308
309    /// Truncate text to fit within a given number of wrapped lines.
310    ///
311    /// Unlike `truncate_line` which treats the text as a flat width budget
312    /// (`width * max_lines`), this method accounts for word-boundary wrapping:
313    /// it walks through characters once, tracking wrap boundaries and the
314    /// truncation point simultaneously. When text overflows on the last
315    /// allowed line, it truncates there and appends the affix.
316    ///
317    /// For `max_lines == 1`, this delegates to `truncate_line`.
318    pub fn truncate_wrapped_line<'a>(
319        &mut self,
320        text: SharedString,
321        wrap_width: Pixels,
322        max_lines: usize,
323        truncation_affix: &str,
324        runs: &'a [TextRun],
325        truncate_from: TruncateFrom,
326    ) -> (SharedString, Cow<'a, [TextRun]>) {
327        if max_lines <= 1 || truncate_from == TruncateFrom::Start {
328            return self.truncate_line(
329                text,
330                wrap_width * max_lines,
331                truncation_affix,
332                runs,
333                truncate_from,
334            );
335        }
336        if truncate_from == TruncateFrom::Middle {
337            return self.truncate_line(text, wrap_width, truncation_affix, runs, truncate_from);
338        }
339
340        let affix_width: Pixels = truncation_affix
341            .chars()
342            .map(|c| self.width_for_char(c))
343            .sum();
344
345        let mut width = px(0.);
346        let mut line = 0usize;
347        let mut first_non_whitespace_ix = None;
348        let mut last_candidate_ix = 0usize;
349        let mut last_candidate_width = px(0.);
350        let mut last_wrap_ix = 0usize;
351        let mut prev_c = '\0';
352        let mut indent: Option<u32> = None;
353        let mut truncate_ix = 0usize;
354
355        for (ix, c) in text.char_indices() {
356            if c == '\n' {
357                if line >= max_lines - 1 && !text[ix + 1..].trim().is_empty() {
358                    // Newline on the last allowed line with real content
359                    // below. Truncate here.
360                    let truncated = text[..truncate_ix]
361                        .trim_end_matches(|c: char| c.is_whitespace() || c.is_ascii_punctuation());
362                    let result = SharedString::from(format!("{truncated}{truncation_affix}"));
363                    let mut runs = runs.to_vec();
364                    update_runs_after_truncation(
365                        &result,
366                        truncation_affix,
367                        &mut runs,
368                        TruncateFrom::End,
369                    );
370                    return (result, Cow::Owned(runs));
371                }
372
373                // Newline before the last line: it consumes a line.
374                line += 1;
375                width = px(0.);
376                first_non_whitespace_ix = None;
377                last_candidate_ix = 0;
378                last_candidate_width = px(0.);
379                last_wrap_ix = ix + 1;
380                prev_c = '\0';
381                indent = None;
382                truncate_ix = ix + 1;
383                continue;
384            }
385
386            let char_width = self.width_for_char(c);
387
388            if Self::is_word_char(c) {
389                if prev_c == ' ' && first_non_whitespace_ix.is_some() {
390                    last_candidate_ix = ix;
391                    last_candidate_width = width;
392                }
393            } else if c != ' ' && first_non_whitespace_ix.is_some() {
394                last_candidate_ix = ix;
395                last_candidate_width = width;
396            }
397
398            if c != ' ' && first_non_whitespace_ix.is_none() {
399                first_non_whitespace_ix = Some(ix);
400            }
401
402            width += char_width;
403
404            if line < max_lines - 1 {
405                // Before the last line: replicate wrap_line's boundary logic.
406                if width > wrap_width && ix > last_wrap_ix {
407                    if let (None, Some(first_nw)) = (indent, first_non_whitespace_ix) {
408                        indent = Some(Self::MAX_INDENT.min((first_nw - last_wrap_ix) as u32));
409                    }
410
411                    if last_candidate_ix > last_wrap_ix {
412                        last_wrap_ix = last_candidate_ix;
413                        width -= last_candidate_width;
414                        last_candidate_ix = 0;
415                    } else {
416                        last_wrap_ix = ix;
417                        width = char_width;
418                    }
419
420                    if let Some(ind) = indent {
421                        width += self.width_for_char(' ') * ind as f32;
422                    }
423
424                    line += 1;
425                    truncate_ix = last_wrap_ix;
426                }
427            } else {
428                // On the last line: track the furthest point where the affix
429                // still fits, and stop as soon as the line overflows.
430                if width + affix_width <= wrap_width {
431                    truncate_ix = ix + c.len_utf8();
432                }
433
434                if width > wrap_width {
435                    let truncated = text[..truncate_ix]
436                        .trim_end_matches(|c: char| c.is_whitespace() || c.is_ascii_punctuation());
437                    let result = SharedString::from(format!("{truncated}{truncation_affix}"));
438                    let mut runs = runs.to_vec();
439                    update_runs_after_truncation(
440                        &result,
441                        truncation_affix,
442                        &mut runs,
443                        TruncateFrom::End,
444                    );
445                    return (result, Cow::Owned(runs));
446                }
447            }
448
449            prev_c = c;
450        }
451
452        // Text fits within max_lines without truncation.
453        (text, Cow::Borrowed(runs))
454    }
455
456    /// Any character in this list should be treated as a word character,
457    /// meaning it can be part of a word that should not be wrapped.
458    /// Returns whether `c` may be part of a word that should not be wrapped.
459    pub fn is_word_char(c: char) -> bool {
460        // ASCII alphanumeric characters, for English, numbers: `Hello123`, etc.
461        c.is_ascii_alphanumeric() ||
462        // Latin script in Unicode for French, German, Spanish, etc.
463        // Latin-1 Supplement
464        // https://en.wikipedia.org/wiki/Latin-1_Supplement
465        matches!(c, '\u{00C0}'..='\u{00FF}') ||
466        // Latin Extended-A
467        // https://en.wikipedia.org/wiki/Latin_Extended-A
468        matches!(c, '\u{0100}'..='\u{017F}') ||
469        // Latin Extended-B
470        // https://en.wikipedia.org/wiki/Latin_Extended-B
471        matches!(c, '\u{0180}'..='\u{024F}') ||
472        // Cyrillic for Russian, Ukrainian, etc.
473        // https://en.wikipedia.org/wiki/Cyrillic_script_in_Unicode
474        matches!(c, '\u{0400}'..='\u{04FF}') ||
475
476        // Vietnamese (https://vietunicode.sourceforge.net/charset/)
477        matches!(c, '\u{1E00}'..='\u{1EFF}') || // Latin Extended Additional
478        matches!(c, '\u{0300}'..='\u{036F}') || // Combining Diacritical Marks
479
480        // Bengali (https://en.wikipedia.org/wiki/Bengali_(Unicode_block))
481        matches!(c, '\u{0980}'..='\u{09FF}') ||
482
483        // Some other known special characters that should be treated as word characters,
484        // e.g. `a-b`, `var_name`, `I'm`/`won’t`, '@mention`, `#hashtag`, `100%`, `3.1415`,
485        // `2^3`, `a~b`, `a=1`, `Self::new`, etc. Trailing punctuation like `,`, `.`, `:`, `;`
486        // is included so it stays attached to the preceding word when wrapping.
487        matches!(c, '-' | '_' | '.' | '\'' | '’' | '‘' | '$' | '%' | '@' | '#' | '^' | '~' | ',' | '=' | ':' | ';') ||
488        // Closing punctuation never starts a line (UAX #14 LB13: no break
489        // before `!`, `)`, `]`, `}`, closing quotes or an ellipsis) — `plz!`,
490        // `see)`, `quoted”` wrap as one word instead of orphaning the mark on
491        // the next line. `/` and `?` stay break opportunities so long paths
492        // and URLs (`a/b`, `foo?b=2`) can wrap.
493        matches!(c, '!' | ')' | ']' | '}' | '"' | '”' | '»' | '…') ||
494        // `⋯` character is special used in Zed, to keep this at the end of the line.
495        matches!(c, '⋯') ||
496
497        // Non-breaking glue characters
498        matches!(c, '\u{202F}' | '\u{00A0}' | '\u{2011}')
499    }
500
501    /// The advance width of `c` in this wrapper's font, cached after first use.
502    #[inline(always)]
503    pub fn width_for_char(&mut self, c: char) -> Pixels {
504        if (c as u32) < 128 {
505            if let Some(cached_width) = self.cached_ascii_char_widths[c as usize] {
506                cached_width
507            } else {
508                let width = self
509                    .text_system
510                    .layout_width(self.font_id, self.font_size, c);
511                self.cached_ascii_char_widths[c as usize] = Some(width);
512                width
513            }
514        } else if let Some(cached_width) = self.cached_other_char_widths.get(&c) {
515            *cached_width
516        } else {
517            let width = self
518                .text_system
519                .layout_width(self.font_id, self.font_size, c);
520            self.cached_other_char_widths.insert(c, width);
521            width
522        }
523    }
524}
525
526/// Trims `runs` to match `result` after `truncate_line` replaced the removed
527/// text with `ellipsis`.
528pub fn update_runs_after_truncation(
529    result: &str,
530    ellipsis: &str,
531    runs: &mut Vec<TextRun>,
532    truncate_from: TruncateFrom,
533) {
534    let mut truncate_at = result.len() - ellipsis.len();
535    match truncate_from {
536        TruncateFrom::Start => {
537            for (run_index, run) in runs.iter_mut().enumerate().rev() {
538                if run.len <= truncate_at {
539                    truncate_at -= run.len;
540                } else {
541                    run.len = truncate_at + ellipsis.len();
542                    runs.splice(..run_index, std::iter::empty());
543                    break;
544                }
545            }
546        }
547        TruncateFrom::End => {
548            for (run_index, run) in runs.iter_mut().enumerate() {
549                if run.len <= truncate_at {
550                    truncate_at -= run.len;
551                } else {
552                    run.len = truncate_at + ellipsis.len();
553                    runs.truncate(run_index + 1);
554                    break;
555                }
556            }
557        }
558        TruncateFrom::Middle => {
559            unreachable!("Middle truncation calls this function with TruncateFrom::End directly")
560        }
561    }
562}
563
564fn update_runs_after_middle_truncation(
565    ellipsis: &str,
566    runs: &mut Vec<TextRun>,
567    front_end_ix: usize,
568    back_start_ix: usize,
569) {
570    let original_runs = std::mem::take(runs);
571    let mut result_runs: Vec<TextRun> = Vec::with_capacity(original_runs.len());
572
573    // Front segment [0, front_end_ix) + ellipsis: walk forward until the run
574    // that straddles or ends at front_end_ix, then extend that run's length
575    // to include the ellipsis.
576    let mut front_remaining = front_end_ix;
577    let mut front_done = false;
578    for run in &original_runs {
579        if front_done {
580            break;
581        }
582        if run.len <= front_remaining {
583            result_runs.push(run.clone());
584            front_remaining -= run.len;
585        } else {
586            let mut partial = run.clone();
587            partial.len = front_remaining + ellipsis.len();
588            result_runs.push(partial);
589            front_done = true;
590        }
591    }
592    if !front_done {
593        // front_end_ix landed exactly on a run boundary; append ellipsis to
594        // the last front run (or, if the front is empty, to the first back run).
595        if let Some(last) = result_runs.last_mut() {
596            last.len += ellipsis.len();
597        } else if let Some(first) = original_runs.first() {
598            let mut affix_run = first.clone();
599            affix_run.len = ellipsis.len();
600            result_runs.push(affix_run);
601        }
602    }
603
604    // Back segment [back_start_ix, original.len()): skip runs entirely in the
605    // removed middle, keep the rest.
606    let mut byte_pos = 0usize;
607    for run in &original_runs {
608        let run_end = byte_pos + run.len;
609        if run_end > back_start_ix {
610            if byte_pos < back_start_ix {
611                // Run straddles back_start_ix; keep only the tail.
612                let mut partial = run.clone();
613                partial.len = run_end - back_start_ix;
614                result_runs.push(partial);
615            } else {
616                result_runs.push(run.clone());
617            }
618        }
619        byte_pos = run_end;
620    }
621
622    *runs = result_runs;
623}
624
625/// A fragment of a line that can be wrapped.
626pub enum LineFragment<'a> {
627    /// A text fragment consisting of characters.
628    Text {
629        /// The text content of the fragment.
630        text: &'a str,
631    },
632    /// A non-text element with a fixed width.
633    Element {
634        /// The width of the element in pixels.
635        width: Pixels,
636        /// The UTF-8 encoded length of the element.
637        len_utf8: usize,
638    },
639}
640
641impl<'a> LineFragment<'a> {
642    /// Creates a new text fragment from the given text.
643    pub fn text(text: &'a str) -> Self {
644        LineFragment::Text { text }
645    }
646
647    /// Creates a new non-text element with the given width and UTF-8 encoded length.
648    pub fn element(width: Pixels, len_utf8: usize) -> Self {
649        LineFragment::Element { width, len_utf8 }
650    }
651
652    fn wrap_boundary_candidates(&self) -> impl Iterator<Item = WrapBoundaryCandidate> {
653        let text = match self {
654            LineFragment::Text { text } => text,
655            LineFragment::Element { .. } => "\0",
656        };
657        text.chars().map(move |character| {
658            if let LineFragment::Element { width, len_utf8 } = self {
659                WrapBoundaryCandidate::Element {
660                    width: *width,
661                    len_utf8: *len_utf8,
662                }
663            } else {
664                WrapBoundaryCandidate::Char { character }
665            }
666        })
667    }
668}
669
670enum WrapBoundaryCandidate {
671    Char { character: char },
672    Element { width: Pixels, len_utf8: usize },
673}
674
675impl WrapBoundaryCandidate {
676    pub fn len_utf8(&self) -> usize {
677        match self {
678            WrapBoundaryCandidate::Char { character } => character.len_utf8(),
679            WrapBoundaryCandidate::Element { len_utf8: len, .. } => *len,
680        }
681    }
682}
683
684/// A boundary between two lines of text.
685#[derive(Copy, Clone, Debug, PartialEq, Eq)]
686pub struct Boundary {
687    /// The index of the last character in a line
688    pub ix: usize,
689    /// The indent of the next line.
690    pub next_indent: u32,
691}
692
693impl Boundary {
694    /// Creates a boundary ending at byte index `ix`, with `next_indent` spaces.
695    pub fn new(ix: usize, next_indent: u32) -> Self {
696        Self { ix, next_indent }
697    }
698}
699
700/// A line wrapper borrowed from a [`TextSystem`] pool, returned on drop.
701pub struct LineWrapperHandle {
702    wrapper: Option<LineWrapper>,
703    recycle: Option<Box<dyn FnOnce(LineWrapper) + Send + Sync>>,
704}
705
706impl LineWrapperHandle {
707    /// Wraps `wrapper`, passing it back through `recycle` when dropped.
708    pub fn new(
709        wrapper: LineWrapper,
710        recycle: impl FnOnce(LineWrapper) + Send + Sync + 'static,
711    ) -> Self {
712        Self {
713            wrapper: Some(wrapper),
714            recycle: Some(Box::new(recycle)),
715        }
716    }
717}
718
719impl Deref for LineWrapperHandle {
720    type Target = LineWrapper;
721
722    fn deref(&self) -> &Self::Target {
723        self.wrapper.as_ref().expect("wrapper present until drop")
724    }
725}
726
727impl DerefMut for LineWrapperHandle {
728    fn deref_mut(&mut self) -> &mut Self::Target {
729        self.wrapper.as_mut().expect("wrapper present until drop")
730    }
731}
732
733impl Drop for LineWrapperHandle {
734    fn drop(&mut self) {
735        let wrapper = self.wrapper.take().expect("wrapper present until drop");
736        let recycle = self.recycle.take().expect("recycle present until drop");
737        recycle(wrapper);
738    }
739}