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}