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