console.rs

  1use super::{
  2    stack_frame_list::{StackFrameList, StackFrameListEvent},
  3    variable_list::VariableList,
  4};
  5use alacritty_terminal::vte::ansi;
  6use anyhow::Result;
  7use collections::HashMap;
  8use dap::OutputEvent;
  9use editor::{Bias, CompletionProvider, Editor, EditorElement, EditorStyle, ExcerptId};
 10use fuzzy::StringMatchCandidate;
 11use gpui::{
 12    Context, Entity, FocusHandle, Focusable, HighlightStyle, Hsla, Render, Subscription, Task,
 13    TextStyle, WeakEntity,
 14};
 15use language::{Buffer, CodeLabel, ToOffset};
 16use menu::Confirm;
 17use project::{
 18    Completion, CompletionResponse,
 19    debugger::session::{CompletionsQuery, OutputToken, Session, SessionEvent},
 20};
 21use settings::Settings;
 22use std::{cell::RefCell, ops::Range, rc::Rc, usize};
 23use theme::{Theme, ThemeSettings};
 24use ui::{Divider, prelude::*};
 25
 26pub struct Console {
 27    console: Entity<Editor>,
 28    query_bar: Entity<Editor>,
 29    session: Entity<Session>,
 30    _subscriptions: Vec<Subscription>,
 31    variable_list: Entity<VariableList>,
 32    stack_frame_list: Entity<StackFrameList>,
 33    last_token: OutputToken,
 34    update_output_task: Task<()>,
 35    focus_handle: FocusHandle,
 36}
 37
 38impl Console {
 39    pub fn new(
 40        session: Entity<Session>,
 41        stack_frame_list: Entity<StackFrameList>,
 42        variable_list: Entity<VariableList>,
 43        window: &mut Window,
 44        cx: &mut Context<Self>,
 45    ) -> Self {
 46        let console = cx.new(|cx| {
 47            let mut editor = Editor::multi_line(window, cx);
 48            editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
 49            editor.set_read_only(true);
 50            editor.disable_scrollbars_and_minimap(window, cx);
 51            editor.set_show_gutter(false, cx);
 52            editor.set_show_runnables(false, cx);
 53            editor.set_show_breakpoints(false, cx);
 54            editor.set_show_code_actions(false, cx);
 55            editor.set_show_line_numbers(false, cx);
 56            editor.set_show_git_diff_gutter(false, cx);
 57            editor.set_autoindent(false);
 58            editor.set_input_enabled(false);
 59            editor.set_use_autoclose(false);
 60            editor.set_show_wrap_guides(false, cx);
 61            editor.set_show_indent_guides(false, cx);
 62            editor.set_show_edit_predictions(Some(false), window, cx);
 63            editor.set_use_modal_editing(false);
 64            editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
 65            editor
 66        });
 67        let focus_handle = cx.focus_handle();
 68
 69        let this = cx.weak_entity();
 70        let query_bar = cx.new(|cx| {
 71            let mut editor = Editor::single_line(window, cx);
 72            editor.set_placeholder_text("Evaluate an expression", cx);
 73            editor.set_use_autoclose(false);
 74            editor.set_show_gutter(false, cx);
 75            editor.set_show_wrap_guides(false, cx);
 76            editor.set_show_indent_guides(false, cx);
 77            editor.set_completion_provider(Some(Rc::new(ConsoleQueryBarCompletionProvider(this))));
 78
 79            editor
 80        });
 81
 82        let _subscriptions = vec![
 83            cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events),
 84            cx.subscribe_in(&session, window, |this, _, event, window, cx| {
 85                if let SessionEvent::ConsoleOutput = event {
 86                    this.update_output(window, cx)
 87                }
 88            }),
 89            cx.on_focus(&focus_handle, window, |console, window, cx| {
 90                if console.is_running(cx) {
 91                    console.query_bar.focus_handle(cx).focus(window);
 92                }
 93            }),
 94        ];
 95
 96        Self {
 97            session,
 98            console,
 99            query_bar,
100            variable_list,
101            _subscriptions,
102            stack_frame_list,
103            update_output_task: Task::ready(()),
104            last_token: OutputToken(0),
105            focus_handle,
106        }
107    }
108
109    #[cfg(test)]
110    pub(crate) fn editor(&self) -> &Entity<Editor> {
111        &self.console
112    }
113
114    fn is_running(&self, cx: &Context<Self>) -> bool {
115        self.session.read(cx).is_running()
116    }
117
118    fn handle_stack_frame_list_events(
119        &mut self,
120        _: Entity<StackFrameList>,
121        event: &StackFrameListEvent,
122        cx: &mut Context<Self>,
123    ) {
124        match event {
125            StackFrameListEvent::SelectedStackFrameChanged(_) => cx.notify(),
126            StackFrameListEvent::BuiltEntries => {}
127        }
128    }
129
130    pub(crate) fn show_indicator(&self, cx: &App) -> bool {
131        self.session.read(cx).has_new_output(self.last_token)
132    }
133
134    pub fn add_messages<'a>(
135        &mut self,
136        events: impl Iterator<Item = &'a OutputEvent>,
137        window: &mut Window,
138        cx: &mut App,
139    ) {
140        self.console.update(cx, |console, cx| {
141            console.set_read_only(false);
142
143            for event in events {
144                let to_insert = format!("{}\n", event.output.trim_end());
145
146                let mut ansi_handler = ConsoleHandler::default();
147                let mut ansi_processor = ansi::Processor::<ansi::StdSyncHandler>::default();
148
149                let len = console.buffer().read(cx).len(cx);
150                ansi_processor.advance(&mut ansi_handler, to_insert.as_bytes());
151                let output = std::mem::take(&mut ansi_handler.output);
152                let mut spans = std::mem::take(&mut ansi_handler.spans);
153                let mut background_spans = std::mem::take(&mut ansi_handler.background_spans);
154                if ansi_handler.current_range_start < output.len() {
155                    spans.push((
156                        ansi_handler.current_range_start..output.len(),
157                        ansi_handler.current_color,
158                    ));
159                }
160                if ansi_handler.current_background_range_start < output.len() {
161                    background_spans.push((
162                        ansi_handler.current_background_range_start..output.len(),
163                        ansi_handler.current_background_color,
164                    ));
165                }
166                console.move_to_end(&editor::actions::MoveToEnd, window, cx);
167                console.insert(&output, window, cx);
168                let buffer = console.buffer().read(cx).snapshot(cx);
169
170                struct ConsoleAnsiHighlight;
171
172                for (range, color) in spans {
173                    let Some(color) = color else { continue };
174                    let start_offset = len + range.start;
175                    let range = start_offset..len + range.end;
176                    let range = buffer.anchor_after(range.start)..buffer.anchor_before(range.end);
177                    let style = HighlightStyle {
178                        color: Some(terminal_view::terminal_element::convert_color(
179                            &color,
180                            cx.theme(),
181                        )),
182                        ..Default::default()
183                    };
184                    console.highlight_text_key::<ConsoleAnsiHighlight>(
185                        start_offset,
186                        vec![range],
187                        style,
188                        cx,
189                    );
190                }
191
192                for (range, color) in background_spans {
193                    let Some(color) = color else { continue };
194                    let start_offset = len + range.start;
195                    let range = start_offset..len + range.end;
196                    let range = buffer.anchor_after(range.start)..buffer.anchor_before(range.end);
197
198                    let color_fetcher: fn(&Theme) -> Hsla = match color {
199                        // Named and theme defined colors
200                        ansi::Color::Named(n) => match n {
201                            ansi::NamedColor::Black => |theme| theme.colors().terminal_ansi_black,
202                            ansi::NamedColor::Red => |theme| theme.colors().terminal_ansi_red,
203                            ansi::NamedColor::Green => |theme| theme.colors().terminal_ansi_green,
204                            ansi::NamedColor::Yellow => |theme| theme.colors().terminal_ansi_yellow,
205                            ansi::NamedColor::Blue => |theme| theme.colors().terminal_ansi_blue,
206                            ansi::NamedColor::Magenta => {
207                                |theme| theme.colors().terminal_ansi_magenta
208                            }
209                            ansi::NamedColor::Cyan => |theme| theme.colors().terminal_ansi_cyan,
210                            ansi::NamedColor::White => |theme| theme.colors().terminal_ansi_white,
211                            ansi::NamedColor::BrightBlack => {
212                                |theme| theme.colors().terminal_ansi_bright_black
213                            }
214                            ansi::NamedColor::BrightRed => {
215                                |theme| theme.colors().terminal_ansi_bright_red
216                            }
217                            ansi::NamedColor::BrightGreen => {
218                                |theme| theme.colors().terminal_ansi_bright_green
219                            }
220                            ansi::NamedColor::BrightYellow => {
221                                |theme| theme.colors().terminal_ansi_bright_yellow
222                            }
223                            ansi::NamedColor::BrightBlue => {
224                                |theme| theme.colors().terminal_ansi_bright_blue
225                            }
226                            ansi::NamedColor::BrightMagenta => {
227                                |theme| theme.colors().terminal_ansi_bright_magenta
228                            }
229                            ansi::NamedColor::BrightCyan => {
230                                |theme| theme.colors().terminal_ansi_bright_cyan
231                            }
232                            ansi::NamedColor::BrightWhite => {
233                                |theme| theme.colors().terminal_ansi_bright_white
234                            }
235                            ansi::NamedColor::Foreground => {
236                                |theme| theme.colors().terminal_foreground
237                            }
238                            ansi::NamedColor::Background => {
239                                |theme| theme.colors().terminal_background
240                            }
241                            ansi::NamedColor::Cursor => |theme| theme.players().local().cursor,
242                            ansi::NamedColor::DimBlack => {
243                                |theme| theme.colors().terminal_ansi_dim_black
244                            }
245                            ansi::NamedColor::DimRed => {
246                                |theme| theme.colors().terminal_ansi_dim_red
247                            }
248                            ansi::NamedColor::DimGreen => {
249                                |theme| theme.colors().terminal_ansi_dim_green
250                            }
251                            ansi::NamedColor::DimYellow => {
252                                |theme| theme.colors().terminal_ansi_dim_yellow
253                            }
254                            ansi::NamedColor::DimBlue => {
255                                |theme| theme.colors().terminal_ansi_dim_blue
256                            }
257                            ansi::NamedColor::DimMagenta => {
258                                |theme| theme.colors().terminal_ansi_dim_magenta
259                            }
260                            ansi::NamedColor::DimCyan => {
261                                |theme| theme.colors().terminal_ansi_dim_cyan
262                            }
263                            ansi::NamedColor::DimWhite => {
264                                |theme| theme.colors().terminal_ansi_dim_white
265                            }
266                            ansi::NamedColor::BrightForeground => {
267                                |theme| theme.colors().terminal_bright_foreground
268                            }
269                            ansi::NamedColor::DimForeground => {
270                                |theme| theme.colors().terminal_dim_foreground
271                            }
272                        },
273                        // 'True' colors
274                        ansi::Color::Spec(_) => |theme| theme.colors().editor_background,
275                        // 8 bit, indexed colors
276                        ansi::Color::Indexed(i) => {
277                            match i {
278                                // 0-15 are the same as the named colors above
279                                0 => |theme| theme.colors().terminal_ansi_black,
280                                1 => |theme| theme.colors().terminal_ansi_red,
281                                2 => |theme| theme.colors().terminal_ansi_green,
282                                3 => |theme| theme.colors().terminal_ansi_yellow,
283                                4 => |theme| theme.colors().terminal_ansi_blue,
284                                5 => |theme| theme.colors().terminal_ansi_magenta,
285                                6 => |theme| theme.colors().terminal_ansi_cyan,
286                                7 => |theme| theme.colors().terminal_ansi_white,
287                                8 => |theme| theme.colors().terminal_ansi_bright_black,
288                                9 => |theme| theme.colors().terminal_ansi_bright_red,
289                                10 => |theme| theme.colors().terminal_ansi_bright_green,
290                                11 => |theme| theme.colors().terminal_ansi_bright_yellow,
291                                12 => |theme| theme.colors().terminal_ansi_bright_blue,
292                                13 => |theme| theme.colors().terminal_ansi_bright_magenta,
293                                14 => |theme| theme.colors().terminal_ansi_bright_cyan,
294                                15 => |theme| theme.colors().terminal_ansi_bright_white,
295                                // 16-231 are a 6x6x6 RGB color cube, mapped to 0-255 using steps defined by XTerm.
296                                // See: https://github.com/xterm-x11/xterm-snapshots/blob/master/256colres.pl
297                                // 16..=231 => {
298                                //     let (r, g, b) = rgb_for_index(index as u8);
299                                //     rgba_color(
300                                //         if r == 0 { 0 } else { r * 40 + 55 },
301                                //         if g == 0 { 0 } else { g * 40 + 55 },
302                                //         if b == 0 { 0 } else { b * 40 + 55 },
303                                //     )
304                                // }
305                                // 232-255 are a 24-step grayscale ramp from (8, 8, 8) to (238, 238, 238).
306                                // 232..=255 => {
307                                //     let i = index as u8 - 232; // Align index to 0..24
308                                //     let value = i * 10 + 8;
309                                //     rgba_color(value, value, value)
310                                // }
311                                // For compatibility with the alacritty::Colors interface
312                                // See: https://github.com/alacritty/alacritty/blob/master/alacritty_terminal/src/term/color.rs
313                                _ => |_| gpui::black(),
314                            }
315                        }
316                    };
317
318                    console.highlight_background_key::<ConsoleAnsiHighlight>(
319                        start_offset,
320                        &[range],
321                        color_fetcher,
322                        cx,
323                    );
324                }
325            }
326
327            console.set_read_only(true);
328            cx.notify();
329        });
330    }
331
332    pub fn evaluate(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
333        let expression = self.query_bar.update(cx, |editor, cx| {
334            let expression = editor.text(cx);
335            cx.defer_in(window, |editor, window, cx| {
336                editor.clear(window, cx);
337            });
338
339            expression
340        });
341
342        self.session.update(cx, |session, cx| {
343            session
344                .evaluate(
345                    expression,
346                    Some(dap::EvaluateArgumentsContext::Repl),
347                    self.stack_frame_list.read(cx).opened_stack_frame_id(),
348                    None,
349                    cx,
350                )
351                .detach();
352        });
353    }
354
355    fn render_console(&self, cx: &Context<Self>) -> impl IntoElement {
356        EditorElement::new(&self.console, Self::editor_style(&self.console, cx))
357    }
358
359    fn editor_style(editor: &Entity<Editor>, cx: &Context<Self>) -> EditorStyle {
360        let is_read_only = editor.read(cx).read_only(cx);
361        let settings = ThemeSettings::get_global(cx);
362        let theme = cx.theme();
363        let text_style = TextStyle {
364            color: if is_read_only {
365                theme.colors().text_muted
366            } else {
367                theme.colors().text
368            },
369            font_family: settings.buffer_font.family.clone(),
370            font_features: settings.buffer_font.features.clone(),
371            font_size: settings.buffer_font_size(cx).into(),
372            font_weight: settings.buffer_font.weight,
373            line_height: relative(settings.buffer_line_height.value()),
374            ..Default::default()
375        };
376        EditorStyle {
377            background: theme.colors().editor_background,
378            local_player: theme.players().local(),
379            text: text_style,
380            ..Default::default()
381        }
382    }
383
384    fn render_query_bar(&self, cx: &Context<Self>) -> impl IntoElement {
385        EditorElement::new(&self.query_bar, Self::editor_style(&self.query_bar, cx))
386    }
387
388    fn update_output(&mut self, window: &mut Window, cx: &mut Context<Self>) {
389        let session = self.session.clone();
390        let token = self.last_token;
391
392        self.update_output_task = cx.spawn_in(window, async move |this, cx| {
393            _ = session.update_in(cx, move |session, window, cx| {
394                let (output, last_processed_token) = session.output(token);
395
396                _ = this.update(cx, |this, cx| {
397                    if last_processed_token == this.last_token {
398                        return;
399                    }
400                    this.add_messages(output, window, cx);
401
402                    this.last_token = last_processed_token;
403                });
404            });
405        });
406    }
407}
408
409impl Render for Console {
410    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
411        v_flex()
412            .track_focus(&self.focus_handle)
413            .key_context("DebugConsole")
414            .on_action(cx.listener(Self::evaluate))
415            .size_full()
416            .child(self.render_console(cx))
417            .when(self.is_running(cx), |this| {
418                this.child(Divider::horizontal())
419                    .child(self.render_query_bar(cx))
420            })
421            .border_2()
422    }
423}
424
425impl Focusable for Console {
426    fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
427        self.focus_handle.clone()
428    }
429}
430
431struct ConsoleQueryBarCompletionProvider(WeakEntity<Console>);
432
433impl CompletionProvider for ConsoleQueryBarCompletionProvider {
434    fn completions(
435        &self,
436        _excerpt_id: ExcerptId,
437        buffer: &Entity<Buffer>,
438        buffer_position: language::Anchor,
439        _trigger: editor::CompletionContext,
440        _window: &mut Window,
441        cx: &mut Context<Editor>,
442    ) -> Task<Result<Vec<CompletionResponse>>> {
443        let Some(console) = self.0.upgrade() else {
444            return Task::ready(Ok(Vec::new()));
445        };
446
447        let support_completions = console
448            .read(cx)
449            .session
450            .read(cx)
451            .capabilities()
452            .supports_completions_request
453            .unwrap_or_default();
454
455        if support_completions {
456            self.client_completions(&console, buffer, buffer_position, cx)
457        } else {
458            self.variable_list_completions(&console, buffer, buffer_position, cx)
459        }
460    }
461
462    fn apply_additional_edits_for_completion(
463        &self,
464        _buffer: Entity<Buffer>,
465        _completions: Rc<RefCell<Box<[Completion]>>>,
466        _completion_index: usize,
467        _push_to_history: bool,
468        _cx: &mut Context<Editor>,
469    ) -> gpui::Task<anyhow::Result<Option<language::Transaction>>> {
470        Task::ready(Ok(None))
471    }
472
473    fn is_completion_trigger(
474        &self,
475        _buffer: &Entity<Buffer>,
476        _position: language::Anchor,
477        _text: &str,
478        _trigger_in_words: bool,
479        _menu_is_open: bool,
480        _cx: &mut Context<Editor>,
481    ) -> bool {
482        true
483    }
484}
485
486impl ConsoleQueryBarCompletionProvider {
487    fn variable_list_completions(
488        &self,
489        console: &Entity<Console>,
490        buffer: &Entity<Buffer>,
491        buffer_position: language::Anchor,
492        cx: &mut Context<Editor>,
493    ) -> Task<Result<Vec<CompletionResponse>>> {
494        let (variables, string_matches) = console.update(cx, |console, cx| {
495            let mut variables = HashMap::default();
496            let mut string_matches = Vec::default();
497
498            for variable in console.variable_list.update(cx, |variable_list, cx| {
499                variable_list.completion_variables(cx)
500            }) {
501                if let Some(evaluate_name) = &variable.evaluate_name {
502                    variables.insert(evaluate_name.clone(), variable.value.clone());
503                    string_matches.push(StringMatchCandidate {
504                        id: 0,
505                        string: evaluate_name.clone(),
506                        char_bag: evaluate_name.chars().collect(),
507                    });
508                }
509
510                variables.insert(variable.name.clone(), variable.value.clone());
511
512                string_matches.push(StringMatchCandidate {
513                    id: 0,
514                    string: variable.name.clone(),
515                    char_bag: variable.name.chars().collect(),
516                });
517            }
518
519            (variables, string_matches)
520        });
521
522        let query = buffer.read(cx).text();
523
524        cx.spawn(async move |_, cx| {
525            const LIMIT: usize = 10;
526            let matches = fuzzy::match_strings(
527                &string_matches,
528                &query,
529                true,
530                true,
531                LIMIT,
532                &Default::default(),
533                cx.background_executor().clone(),
534            )
535            .await;
536
537            let completions = matches
538                .iter()
539                .filter_map(|string_match| {
540                    let variable_value = variables.get(&string_match.string)?;
541
542                    Some(project::Completion {
543                        replace_range: buffer_position..buffer_position,
544                        new_text: string_match.string.clone(),
545                        label: CodeLabel {
546                            filter_range: 0..string_match.string.len(),
547                            text: format!("{} {}", string_match.string, variable_value),
548                            runs: Vec::new(),
549                        },
550                        icon_path: None,
551                        documentation: None,
552                        confirm: None,
553                        source: project::CompletionSource::Custom,
554                        insert_text_mode: None,
555                    })
556                })
557                .collect::<Vec<_>>();
558
559            Ok(vec![project::CompletionResponse {
560                is_incomplete: completions.len() >= LIMIT,
561                completions,
562            }])
563        })
564    }
565
566    fn client_completions(
567        &self,
568        console: &Entity<Console>,
569        buffer: &Entity<Buffer>,
570        buffer_position: language::Anchor,
571        cx: &mut Context<Editor>,
572    ) -> Task<Result<Vec<CompletionResponse>>> {
573        let completion_task = console.update(cx, |console, cx| {
574            console.session.update(cx, |state, cx| {
575                let frame_id = console.stack_frame_list.read(cx).opened_stack_frame_id();
576
577                state.completions(
578                    CompletionsQuery::new(buffer.read(cx), buffer_position, frame_id),
579                    cx,
580                )
581            })
582        });
583        let snapshot = buffer.read(cx).text_snapshot();
584        cx.background_executor().spawn(async move {
585            let completions = completion_task.await?;
586
587            let completions = completions
588                .into_iter()
589                .map(|completion| {
590                    let new_text = completion
591                        .text
592                        .as_ref()
593                        .unwrap_or(&completion.label)
594                        .to_owned();
595                    let buffer_text = snapshot.text();
596                    let buffer_bytes = buffer_text.as_bytes();
597                    let new_bytes = new_text.as_bytes();
598
599                    let mut prefix_len = 0;
600                    for i in (0..new_bytes.len()).rev() {
601                        if buffer_bytes.ends_with(&new_bytes[0..i]) {
602                            prefix_len = i;
603                            break;
604                        }
605                    }
606
607                    let buffer_offset = buffer_position.to_offset(&snapshot);
608                    let start = buffer_offset - prefix_len;
609                    let start = snapshot.clip_offset(start, Bias::Left);
610                    let start = snapshot.anchor_before(start);
611                    let replace_range = start..buffer_position;
612
613                    project::Completion {
614                        replace_range,
615                        new_text,
616                        label: CodeLabel {
617                            filter_range: 0..completion.label.len(),
618                            text: completion.label,
619                            runs: Vec::new(),
620                        },
621                        icon_path: None,
622                        documentation: None,
623                        confirm: None,
624                        source: project::CompletionSource::BufferWord {
625                            word_range: buffer_position..language::Anchor::MAX,
626                            resolved: false,
627                        },
628                        insert_text_mode: None,
629                    }
630                })
631                .collect();
632
633            Ok(vec![project::CompletionResponse {
634                completions,
635                is_incomplete: false,
636            }])
637        })
638    }
639}
640
641#[derive(Default)]
642struct ConsoleHandler {
643    output: String,
644    spans: Vec<(Range<usize>, Option<ansi::Color>)>,
645    background_spans: Vec<(Range<usize>, Option<ansi::Color>)>,
646    current_range_start: usize,
647    current_background_range_start: usize,
648    current_color: Option<ansi::Color>,
649    current_background_color: Option<ansi::Color>,
650    pos: usize,
651}
652
653impl ConsoleHandler {
654    fn break_span(&mut self, color: Option<ansi::Color>) {
655        self.spans.push((
656            self.current_range_start..self.output.len(),
657            self.current_color,
658        ));
659        self.current_color = color;
660        self.current_range_start = self.pos;
661    }
662
663    fn break_background_span(&mut self, color: Option<ansi::Color>) {
664        self.background_spans.push((
665            self.current_background_range_start..self.output.len(),
666            self.current_background_color,
667        ));
668        self.current_background_color = color;
669        self.current_background_range_start = self.pos;
670    }
671}
672
673impl ansi::Handler for ConsoleHandler {
674    fn input(&mut self, c: char) {
675        self.output.push(c);
676        self.pos += c.len_utf8();
677    }
678
679    fn linefeed(&mut self) {
680        self.output.push('\n');
681        self.pos += 1;
682    }
683
684    fn put_tab(&mut self, count: u16) {
685        self.output
686            .extend(std::iter::repeat('\t').take(count as usize));
687        self.pos += count as usize;
688    }
689
690    fn terminal_attribute(&mut self, attr: ansi::Attr) {
691        match attr {
692            ansi::Attr::Foreground(color) => {
693                self.break_span(Some(color));
694            }
695            ansi::Attr::Background(color) => {
696                self.break_background_span(Some(color));
697            }
698            ansi::Attr::Reset => {
699                self.break_span(None);
700                self.break_background_span(None);
701            }
702            _ => {}
703        }
704    }
705}