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::{CompletionItem, CompletionItemType, OutputEvent};
9use editor::{
10 Bias, CompletionProvider, Editor, EditorElement, EditorMode, EditorStyle, ExcerptId,
11 SizingBehavior,
12};
13use fuzzy::StringMatchCandidate;
14use gpui::{
15 Action as _, AppContext, Context, Corner, Entity, FocusHandle, Focusable, HighlightStyle, Hsla,
16 Render, Subscription, Task, TextStyle, WeakEntity, actions,
17};
18use language::{Anchor, Buffer, CharScopeContext, CodeLabel, TextBufferSnapshot, ToOffset};
19use menu::{Confirm, SelectNext, SelectPrevious};
20use project::{
21 Completion, CompletionDisplayOptions, CompletionResponse,
22 debugger::session::{CompletionsQuery, OutputToken, Session},
23 lsp_store::CompletionDocumentation,
24 search_history::{SearchHistory, SearchHistoryCursor},
25};
26use settings::Settings;
27use std::fmt::Write;
28use std::{cell::RefCell, ops::Range, rc::Rc, usize};
29use theme::{Theme, ThemeSettings};
30use ui::{ContextMenu, Divider, PopoverMenu, SplitButton, Tooltip, prelude::*};
31use util::ResultExt;
32
33actions!(
34 console,
35 [
36 /// Adds an expression to the watch list.
37 WatchExpression
38 ]
39);
40
41pub struct Console {
42 console: Entity<Editor>,
43 query_bar: Entity<Editor>,
44 session: Entity<Session>,
45 _subscriptions: Vec<Subscription>,
46 variable_list: Entity<VariableList>,
47 stack_frame_list: Entity<StackFrameList>,
48 last_token: OutputToken,
49 update_output_task: Option<Task<()>>,
50 focus_handle: FocusHandle,
51 history: SearchHistory,
52 cursor: SearchHistoryCursor,
53}
54
55impl Console {
56 pub fn new(
57 session: Entity<Session>,
58 stack_frame_list: Entity<StackFrameList>,
59 variable_list: Entity<VariableList>,
60 window: &mut Window,
61 cx: &mut Context<Self>,
62 ) -> Self {
63 let console = cx.new(|cx| {
64 let mut editor = Editor::multi_line(window, cx);
65 editor.set_mode(EditorMode::Full {
66 scale_ui_elements_with_buffer_font_size: true,
67 show_active_line_background: true,
68 sizing_behavior: SizingBehavior::ExcludeOverscrollMargin,
69 });
70 editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
71 editor.set_read_only(true);
72 editor.disable_scrollbars_and_minimap(window, cx);
73 editor.set_show_gutter(false, cx);
74 editor.set_show_runnables(false, cx);
75 editor.set_show_breakpoints(false, cx);
76 editor.set_show_code_actions(false, cx);
77 editor.set_show_line_numbers(false, cx);
78 editor.set_show_git_diff_gutter(false, cx);
79 editor.set_autoindent(false);
80 editor.set_input_enabled(false);
81 editor.set_use_autoclose(false);
82 editor.set_show_wrap_guides(false, cx);
83 editor.set_show_indent_guides(false, cx);
84 editor.set_show_edit_predictions(Some(false), window, cx);
85 editor.set_use_modal_editing(false);
86 editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
87 editor
88 });
89 let focus_handle = cx.focus_handle();
90
91 let this = cx.weak_entity();
92 let query_bar = cx.new(|cx| {
93 let mut editor = Editor::single_line(window, cx);
94 editor.set_placeholder_text("Evaluate an expression", window, cx);
95 editor.set_use_autoclose(false);
96 editor.set_show_gutter(false, cx);
97 editor.set_show_wrap_guides(false, cx);
98 editor.set_show_indent_guides(false, cx);
99 editor.set_completion_provider(Some(Rc::new(ConsoleQueryBarCompletionProvider(this))));
100
101 editor
102 });
103
104 let _subscriptions = vec![
105 cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events),
106 cx.on_focus(&focus_handle, window, |console, window, cx| {
107 if console.is_running(cx) {
108 console.query_bar.focus_handle(cx).focus(window);
109 }
110 }),
111 ];
112
113 Self {
114 session,
115 console,
116 query_bar,
117 variable_list,
118 _subscriptions,
119 stack_frame_list,
120 update_output_task: None,
121 last_token: OutputToken(0),
122 focus_handle,
123 history: SearchHistory::new(
124 None,
125 project::search_history::QueryInsertionBehavior::ReplacePreviousIfContains,
126 ),
127 cursor: Default::default(),
128 }
129 }
130
131 #[cfg(test)]
132 pub(crate) fn editor(&self) -> &Entity<Editor> {
133 &self.console
134 }
135
136 fn is_running(&self, cx: &Context<Self>) -> bool {
137 self.session.read(cx).is_started()
138 }
139
140 fn handle_stack_frame_list_events(
141 &mut self,
142 _: Entity<StackFrameList>,
143 event: &StackFrameListEvent,
144 cx: &mut Context<Self>,
145 ) {
146 match event {
147 StackFrameListEvent::SelectedStackFrameChanged(_) => cx.notify(),
148 StackFrameListEvent::BuiltEntries => {}
149 }
150 }
151
152 pub(crate) fn show_indicator(&self, cx: &App) -> bool {
153 self.session.read(cx).has_new_output(self.last_token)
154 }
155
156 fn add_messages(
157 &mut self,
158 events: Vec<OutputEvent>,
159 window: &mut Window,
160 cx: &mut App,
161 ) -> Task<Result<()>> {
162 self.console.update(cx, |_, cx| {
163 cx.spawn_in(window, async move |console, cx| {
164 let mut len = console.update(cx, |this, cx| this.buffer().read(cx).len(cx))?;
165 let (output, spans, background_spans) = cx
166 .background_spawn(async move {
167 let mut all_spans = Vec::new();
168 let mut all_background_spans = Vec::new();
169 let mut to_insert = String::new();
170 let mut scratch = String::new();
171
172 for event in &events {
173 scratch.clear();
174 let mut ansi_handler = ConsoleHandler::default();
175 let mut ansi_processor =
176 ansi::Processor::<ansi::StdSyncHandler>::default();
177
178 let trimmed_output = event.output.trim_end();
179 let _ = writeln!(&mut scratch, "{trimmed_output}");
180 ansi_processor.advance(&mut ansi_handler, scratch.as_bytes());
181 let output = std::mem::take(&mut ansi_handler.output);
182 to_insert.extend(output.chars());
183 let mut spans = std::mem::take(&mut ansi_handler.spans);
184 let mut background_spans =
185 std::mem::take(&mut ansi_handler.background_spans);
186 if ansi_handler.current_range_start < output.len() {
187 spans.push((
188 ansi_handler.current_range_start..output.len(),
189 ansi_handler.current_color,
190 ));
191 }
192 if ansi_handler.current_background_range_start < output.len() {
193 background_spans.push((
194 ansi_handler.current_background_range_start..output.len(),
195 ansi_handler.current_background_color,
196 ));
197 }
198
199 for (range, _) in spans.iter_mut() {
200 let start_offset = len + range.start;
201 *range = start_offset..len + range.end;
202 }
203
204 for (range, _) in background_spans.iter_mut() {
205 let start_offset = len + range.start;
206 *range = start_offset..len + range.end;
207 }
208
209 len += output.len();
210
211 all_spans.extend(spans);
212 all_background_spans.extend(background_spans);
213 }
214 (to_insert, all_spans, all_background_spans)
215 })
216 .await;
217 console.update_in(cx, |console, window, cx| {
218 console.set_read_only(false);
219 console.move_to_end(&editor::actions::MoveToEnd, window, cx);
220 console.insert(&output, window, cx);
221 console.set_read_only(true);
222
223 struct ConsoleAnsiHighlight;
224
225 let buffer = console.buffer().read(cx).snapshot(cx);
226
227 for (range, color) in spans {
228 let Some(color) = color else { continue };
229 let start_offset = range.start;
230 let range =
231 buffer.anchor_after(range.start)..buffer.anchor_before(range.end);
232 let style = HighlightStyle {
233 color: Some(terminal_view::terminal_element::convert_color(
234 &color,
235 cx.theme(),
236 )),
237 ..Default::default()
238 };
239 console.highlight_text_key::<ConsoleAnsiHighlight>(
240 start_offset,
241 vec![range],
242 style,
243 cx,
244 );
245 }
246
247 for (range, color) in background_spans {
248 let Some(color) = color else { continue };
249 let start_offset = range.start;
250 let range =
251 buffer.anchor_after(range.start)..buffer.anchor_before(range.end);
252 console.highlight_background_key::<ConsoleAnsiHighlight>(
253 start_offset,
254 &[range],
255 color_fetcher(color),
256 cx,
257 );
258 }
259
260 cx.notify();
261 })?;
262
263 Ok(())
264 })
265 })
266 }
267
268 pub fn watch_expression(
269 &mut self,
270 _: &WatchExpression,
271 window: &mut Window,
272 cx: &mut Context<Self>,
273 ) {
274 let expression = self.query_bar.update(cx, |editor, cx| {
275 let expression = editor.text(cx);
276 cx.defer_in(window, |editor, window, cx| {
277 editor.clear(window, cx);
278 });
279
280 expression
281 });
282 self.history.add(&mut self.cursor, expression.clone());
283 self.cursor.reset();
284 self.session.update(cx, |session, cx| {
285 session
286 .evaluate(
287 expression.clone(),
288 Some(dap::EvaluateArgumentsContext::Repl),
289 self.stack_frame_list.read(cx).opened_stack_frame_id(),
290 None,
291 cx,
292 )
293 .detach();
294
295 if let Some(stack_frame_id) = self.stack_frame_list.read(cx).opened_stack_frame_id() {
296 session
297 .add_watcher(expression.into(), stack_frame_id, cx)
298 .detach();
299 }
300 });
301 }
302
303 fn previous_query(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) {
304 let prev = self.history.previous(&mut self.cursor);
305 if let Some(prev) = prev {
306 self.query_bar.update(cx, |editor, cx| {
307 editor.set_text(prev, window, cx);
308 });
309 }
310 }
311
312 fn next_query(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
313 let next = self.history.next(&mut self.cursor);
314 let query = next.unwrap_or_else(|| {
315 self.cursor.reset();
316 ""
317 });
318
319 self.query_bar.update(cx, |editor, cx| {
320 editor.set_text(query, window, cx);
321 });
322 }
323
324 fn evaluate(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
325 let expression = self.query_bar.update(cx, |editor, cx| {
326 let expression = editor.text(cx);
327 cx.defer_in(window, |editor, window, cx| {
328 editor.clear(window, cx);
329 });
330
331 expression
332 });
333
334 self.history.add(&mut self.cursor, expression.clone());
335 self.cursor.reset();
336 self.session.update(cx, |session, cx| {
337 session
338 .evaluate(
339 expression,
340 Some(dap::EvaluateArgumentsContext::Repl),
341 self.stack_frame_list.read(cx).opened_stack_frame_id(),
342 None,
343 cx,
344 )
345 .detach();
346 });
347 }
348
349 fn render_submit_menu(
350 &self,
351 id: impl Into<ElementId>,
352 keybinding_target: Option<FocusHandle>,
353 cx: &App,
354 ) -> impl IntoElement {
355 PopoverMenu::new(id.into())
356 .trigger(
357 ui::ButtonLike::new_rounded_right("console-confirm-split-button-right")
358 .layer(ui::ElevationIndex::ModalSurface)
359 .size(ui::ButtonSize::None)
360 .child(
361 div()
362 .px_1()
363 .child(Icon::new(IconName::ChevronDown).size(IconSize::XSmall)),
364 ),
365 )
366 .when(
367 self.stack_frame_list
368 .read(cx)
369 .opened_stack_frame_id()
370 .is_some(),
371 |this| {
372 this.menu(move |window, cx| {
373 Some(ContextMenu::build(window, cx, |context_menu, _, _| {
374 context_menu
375 .when_some(keybinding_target.clone(), |el, keybinding_target| {
376 el.context(keybinding_target)
377 })
378 .action("Watch Expression", WatchExpression.boxed_clone())
379 }))
380 })
381 },
382 )
383 .anchor(Corner::TopRight)
384 }
385
386 fn render_console(&self, cx: &Context<Self>) -> impl IntoElement {
387 EditorElement::new(&self.console, Self::editor_style(&self.console, cx))
388 }
389
390 fn editor_style(editor: &Entity<Editor>, cx: &Context<Self>) -> EditorStyle {
391 let is_read_only = editor.read(cx).read_only(cx);
392 let settings = ThemeSettings::get_global(cx);
393 let theme = cx.theme();
394 let text_style = TextStyle {
395 color: if is_read_only {
396 theme.colors().text_muted
397 } else {
398 theme.colors().text
399 },
400 font_family: settings.buffer_font.family.clone(),
401 font_features: settings.buffer_font.features.clone(),
402 font_size: settings.buffer_font_size(cx).into(),
403 font_weight: settings.buffer_font.weight,
404 line_height: relative(settings.buffer_line_height.value()),
405 ..Default::default()
406 };
407 EditorStyle {
408 background: theme.colors().editor_background,
409 local_player: theme.players().local(),
410 text: text_style,
411 ..Default::default()
412 }
413 }
414
415 fn render_query_bar(&self, cx: &Context<Self>) -> impl IntoElement {
416 EditorElement::new(&self.query_bar, Self::editor_style(&self.query_bar, cx))
417 }
418
419 pub(crate) fn update_output(&mut self, window: &mut Window, cx: &mut Context<Self>) {
420 if self.update_output_task.is_some() {
421 return;
422 }
423 let session = self.session.clone();
424 let token = self.last_token;
425 self.update_output_task = Some(cx.spawn_in(window, async move |this, cx| {
426 let Some((last_processed_token, task)) = session
427 .update_in(cx, |session, window, cx| {
428 let (output, last_processed_token) = session.output(token);
429
430 this.update(cx, |this, cx| {
431 if last_processed_token == this.last_token {
432 return None;
433 }
434 Some((
435 last_processed_token,
436 this.add_messages(output.cloned().collect(), window, cx),
437 ))
438 })
439 .ok()
440 .flatten()
441 })
442 .ok()
443 .flatten()
444 else {
445 _ = this.update(cx, |this, _| {
446 this.update_output_task.take();
447 });
448 return;
449 };
450 _ = task.await.log_err();
451 _ = this.update(cx, |this, _| {
452 this.last_token = last_processed_token;
453 this.update_output_task.take();
454 });
455 }));
456 }
457}
458
459impl Render for Console {
460 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
461 let query_focus_handle = self.query_bar.focus_handle(cx);
462 self.update_output(window, cx);
463
464 v_flex()
465 .track_focus(&self.focus_handle)
466 .key_context("DebugConsole")
467 .on_action(cx.listener(Self::evaluate))
468 .on_action(cx.listener(Self::watch_expression))
469 .size_full()
470 .border_2()
471 .bg(cx.theme().colors().editor_background)
472 .child(self.render_console(cx))
473 .when(self.is_running(cx), |this| {
474 this.child(Divider::horizontal()).child(
475 h_flex()
476 .on_action(cx.listener(Self::previous_query))
477 .on_action(cx.listener(Self::next_query))
478 .p_1()
479 .gap_1()
480 .bg(cx.theme().colors().editor_background)
481 .child(self.render_query_bar(cx))
482 .child(SplitButton::new(
483 ui::ButtonLike::new_rounded_all(ElementId::Name(
484 "split-button-left-confirm-button".into(),
485 ))
486 .on_click(move |_, window, cx| {
487 window.dispatch_action(Box::new(Confirm), cx)
488 })
489 .layer(ui::ElevationIndex::ModalSurface)
490 .size(ui::ButtonSize::Compact)
491 .child(Label::new("Evaluate"))
492 .tooltip({
493 let query_focus_handle = query_focus_handle.clone();
494
495 move |_window, cx| {
496 Tooltip::for_action_in(
497 "Evaluate",
498 &Confirm,
499 &query_focus_handle,
500 cx,
501 )
502 }
503 }),
504 self.render_submit_menu(
505 ElementId::Name("split-button-right-confirm-button".into()),
506 Some(query_focus_handle.clone()),
507 cx,
508 )
509 .into_any_element(),
510 )),
511 )
512 })
513 }
514}
515
516impl Focusable for Console {
517 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
518 self.focus_handle.clone()
519 }
520}
521
522struct ConsoleQueryBarCompletionProvider(WeakEntity<Console>);
523
524impl CompletionProvider for ConsoleQueryBarCompletionProvider {
525 fn completions(
526 &self,
527 _excerpt_id: ExcerptId,
528 buffer: &Entity<Buffer>,
529 buffer_position: language::Anchor,
530 _trigger: editor::CompletionContext,
531 _window: &mut Window,
532 cx: &mut Context<Editor>,
533 ) -> Task<Result<Vec<CompletionResponse>>> {
534 let Some(console) = self.0.upgrade() else {
535 return Task::ready(Ok(Vec::new()));
536 };
537
538 let support_completions = console
539 .read(cx)
540 .session
541 .read(cx)
542 .capabilities()
543 .supports_completions_request
544 .unwrap_or_default();
545
546 if support_completions {
547 self.client_completions(&console, buffer, buffer_position, cx)
548 } else {
549 self.variable_list_completions(&console, buffer, buffer_position, cx)
550 }
551 }
552
553 fn apply_additional_edits_for_completion(
554 &self,
555 _buffer: Entity<Buffer>,
556 _completions: Rc<RefCell<Box<[Completion]>>>,
557 _completion_index: usize,
558 _push_to_history: bool,
559 _cx: &mut Context<Editor>,
560 ) -> gpui::Task<anyhow::Result<Option<language::Transaction>>> {
561 Task::ready(Ok(None))
562 }
563
564 fn is_completion_trigger(
565 &self,
566 buffer: &Entity<Buffer>,
567 position: language::Anchor,
568 text: &str,
569 trigger_in_words: bool,
570 menu_is_open: bool,
571 cx: &mut Context<Editor>,
572 ) -> bool {
573 let mut chars = text.chars();
574 let char = if let Some(char) = chars.next() {
575 char
576 } else {
577 return false;
578 };
579
580 let snapshot = buffer.read(cx).snapshot();
581 if !menu_is_open && !snapshot.settings_at(position, cx).show_completions_on_input {
582 return false;
583 }
584
585 let classifier = snapshot
586 .char_classifier_at(position)
587 .scope_context(Some(CharScopeContext::Completion));
588 if trigger_in_words && classifier.is_word(char) {
589 return true;
590 }
591
592 self.0
593 .read_with(cx, |console, cx| {
594 console
595 .session
596 .read(cx)
597 .capabilities()
598 .completion_trigger_characters
599 .as_ref()
600 .map(|triggers| triggers.contains(&text.to_string()))
601 })
602 .ok()
603 .flatten()
604 .unwrap_or(true)
605 }
606}
607
608impl ConsoleQueryBarCompletionProvider {
609 fn variable_list_completions(
610 &self,
611 console: &Entity<Console>,
612 buffer: &Entity<Buffer>,
613 buffer_position: language::Anchor,
614 cx: &mut Context<Editor>,
615 ) -> Task<Result<Vec<CompletionResponse>>> {
616 let (variables, string_matches) = console.update(cx, |console, cx| {
617 let mut variables = HashMap::default();
618 let mut string_matches = Vec::default();
619
620 for variable in console.variable_list.update(cx, |variable_list, cx| {
621 variable_list.completion_variables(cx)
622 }) {
623 if let Some(evaluate_name) = &variable.evaluate_name
624 && variables
625 .insert(evaluate_name.clone(), variable.value.clone())
626 .is_none()
627 {
628 string_matches.push(StringMatchCandidate {
629 id: 0,
630 string: evaluate_name.clone(),
631 char_bag: evaluate_name.chars().collect(),
632 });
633 }
634
635 if variables
636 .insert(variable.name.clone(), variable.value.clone())
637 .is_none()
638 {
639 string_matches.push(StringMatchCandidate {
640 id: 0,
641 string: variable.name.clone(),
642 char_bag: variable.name.chars().collect(),
643 });
644 }
645 }
646
647 (variables, string_matches)
648 });
649
650 let snapshot = buffer.read(cx).text_snapshot();
651 let buffer_text = snapshot.text();
652
653 cx.spawn(async move |_, cx| {
654 const LIMIT: usize = 10;
655 let matches = fuzzy::match_strings(
656 &string_matches,
657 &buffer_text,
658 true,
659 true,
660 LIMIT,
661 &Default::default(),
662 cx.background_executor().clone(),
663 )
664 .await;
665
666 let completions = matches
667 .iter()
668 .filter_map(|string_match| {
669 let variable_value = variables.get(&string_match.string)?;
670
671 Some(project::Completion {
672 replace_range: Self::replace_range_for_completion(
673 &buffer_text,
674 buffer_position,
675 string_match.string.as_bytes(),
676 &snapshot,
677 ),
678 new_text: string_match.string.clone(),
679 label: CodeLabel::plain(string_match.string.clone(), None),
680 icon_path: None,
681 documentation: Some(CompletionDocumentation::MultiLineMarkdown(
682 variable_value.into(),
683 )),
684 confirm: None,
685 source: project::CompletionSource::Custom,
686 insert_text_mode: None,
687 })
688 })
689 .collect::<Vec<_>>();
690
691 Ok(vec![project::CompletionResponse {
692 is_incomplete: completions.len() >= LIMIT,
693 display_options: CompletionDisplayOptions::default(),
694 completions,
695 }])
696 })
697 }
698
699 fn replace_range_for_completion(
700 buffer_text: &String,
701 buffer_position: Anchor,
702 new_bytes: &[u8],
703 snapshot: &TextBufferSnapshot,
704 ) -> Range<Anchor> {
705 let buffer_offset = buffer_position.to_offset(snapshot);
706 let buffer_bytes = &buffer_text.as_bytes()[0..buffer_offset];
707
708 let mut prefix_len = 0;
709 for i in (0..new_bytes.len()).rev() {
710 if buffer_bytes.ends_with(&new_bytes[0..i]) {
711 prefix_len = i;
712 break;
713 }
714 }
715
716 let start = snapshot.clip_offset(buffer_offset - prefix_len, Bias::Left);
717
718 snapshot.anchor_before(start)..buffer_position
719 }
720
721 const fn completion_type_score(completion_type: CompletionItemType) -> usize {
722 match completion_type {
723 CompletionItemType::Field | CompletionItemType::Property => 0,
724 CompletionItemType::Variable | CompletionItemType::Value => 1,
725 CompletionItemType::Method
726 | CompletionItemType::Function
727 | CompletionItemType::Constructor => 2,
728 CompletionItemType::Class
729 | CompletionItemType::Interface
730 | CompletionItemType::Module => 3,
731 _ => 4,
732 }
733 }
734
735 fn completion_item_sort_text(completion_item: &CompletionItem) -> String {
736 completion_item.sort_text.clone().unwrap_or_else(|| {
737 format!(
738 "{:03}_{}",
739 Self::completion_type_score(
740 completion_item.type_.unwrap_or(CompletionItemType::Text)
741 ),
742 completion_item.label.to_ascii_lowercase()
743 )
744 })
745 }
746
747 fn client_completions(
748 &self,
749 console: &Entity<Console>,
750 buffer: &Entity<Buffer>,
751 buffer_position: language::Anchor,
752 cx: &mut Context<Editor>,
753 ) -> Task<Result<Vec<CompletionResponse>>> {
754 let completion_task = console.update(cx, |console, cx| {
755 console.session.update(cx, |state, cx| {
756 let frame_id = console.stack_frame_list.read(cx).opened_stack_frame_id();
757
758 state.completions(
759 CompletionsQuery::new(buffer.read(cx), buffer_position, frame_id),
760 cx,
761 )
762 })
763 });
764 let snapshot = buffer.read(cx).text_snapshot();
765 cx.background_executor().spawn(async move {
766 let completions = completion_task.await?;
767
768 let buffer_text = snapshot.text();
769
770 let completions = completions
771 .into_iter()
772 .map(|completion| {
773 let sort_text = Self::completion_item_sort_text(&completion);
774 let new_text = completion
775 .text
776 .as_ref()
777 .unwrap_or(&completion.label)
778 .to_owned();
779
780 project::Completion {
781 replace_range: Self::replace_range_for_completion(
782 &buffer_text,
783 buffer_position,
784 new_text.as_bytes(),
785 &snapshot,
786 ),
787 new_text,
788 label: CodeLabel::plain(completion.label, None),
789 icon_path: None,
790 documentation: completion.detail.map(|detail| {
791 CompletionDocumentation::MultiLineMarkdown(detail.into())
792 }),
793 confirm: None,
794 source: project::CompletionSource::Dap { sort_text },
795 insert_text_mode: None,
796 }
797 })
798 .collect();
799
800 Ok(vec![project::CompletionResponse {
801 completions,
802 display_options: CompletionDisplayOptions::default(),
803 is_incomplete: false,
804 }])
805 })
806 }
807}
808
809#[derive(Default)]
810struct ConsoleHandler {
811 output: String,
812 spans: Vec<(Range<usize>, Option<ansi::Color>)>,
813 background_spans: Vec<(Range<usize>, Option<ansi::Color>)>,
814 current_range_start: usize,
815 current_background_range_start: usize,
816 current_color: Option<ansi::Color>,
817 current_background_color: Option<ansi::Color>,
818 pos: usize,
819}
820
821impl ConsoleHandler {
822 fn break_span(&mut self, color: Option<ansi::Color>) {
823 self.spans.push((
824 self.current_range_start..self.output.len(),
825 self.current_color,
826 ));
827 self.current_color = color;
828 self.current_range_start = self.pos;
829 }
830
831 fn break_background_span(&mut self, color: Option<ansi::Color>) {
832 self.background_spans.push((
833 self.current_background_range_start..self.output.len(),
834 self.current_background_color,
835 ));
836 self.current_background_color = color;
837 self.current_background_range_start = self.pos;
838 }
839}
840
841impl ansi::Handler for ConsoleHandler {
842 fn input(&mut self, c: char) {
843 self.output.push(c);
844 self.pos += c.len_utf8();
845 }
846
847 fn linefeed(&mut self) {
848 self.output.push('\n');
849 self.pos += 1;
850 }
851
852 fn put_tab(&mut self, count: u16) {
853 self.output
854 .extend(std::iter::repeat('\t').take(count as usize));
855 self.pos += count as usize;
856 }
857
858 fn terminal_attribute(&mut self, attr: ansi::Attr) {
859 match attr {
860 ansi::Attr::Foreground(color) => {
861 self.break_span(Some(color));
862 }
863 ansi::Attr::Background(color) => {
864 self.break_background_span(Some(color));
865 }
866 ansi::Attr::Reset => {
867 self.break_span(None);
868 self.break_background_span(None);
869 }
870 _ => {}
871 }
872 }
873}
874
875fn color_fetcher(color: ansi::Color) -> fn(&Theme) -> Hsla {
876 let color_fetcher: fn(&Theme) -> Hsla = match color {
877 // Named and theme defined colors
878 ansi::Color::Named(n) => match n {
879 ansi::NamedColor::Black => |theme| theme.colors().terminal_ansi_black,
880 ansi::NamedColor::Red => |theme| theme.colors().terminal_ansi_red,
881 ansi::NamedColor::Green => |theme| theme.colors().terminal_ansi_green,
882 ansi::NamedColor::Yellow => |theme| theme.colors().terminal_ansi_yellow,
883 ansi::NamedColor::Blue => |theme| theme.colors().terminal_ansi_blue,
884 ansi::NamedColor::Magenta => |theme| theme.colors().terminal_ansi_magenta,
885 ansi::NamedColor::Cyan => |theme| theme.colors().terminal_ansi_cyan,
886 ansi::NamedColor::White => |theme| theme.colors().terminal_ansi_white,
887 ansi::NamedColor::BrightBlack => |theme| theme.colors().terminal_ansi_bright_black,
888 ansi::NamedColor::BrightRed => |theme| theme.colors().terminal_ansi_bright_red,
889 ansi::NamedColor::BrightGreen => |theme| theme.colors().terminal_ansi_bright_green,
890 ansi::NamedColor::BrightYellow => |theme| theme.colors().terminal_ansi_bright_yellow,
891 ansi::NamedColor::BrightBlue => |theme| theme.colors().terminal_ansi_bright_blue,
892 ansi::NamedColor::BrightMagenta => |theme| theme.colors().terminal_ansi_bright_magenta,
893 ansi::NamedColor::BrightCyan => |theme| theme.colors().terminal_ansi_bright_cyan,
894 ansi::NamedColor::BrightWhite => |theme| theme.colors().terminal_ansi_bright_white,
895 ansi::NamedColor::Foreground => |theme| theme.colors().terminal_foreground,
896 ansi::NamedColor::Background => |theme| theme.colors().terminal_background,
897 ansi::NamedColor::Cursor => |theme| theme.players().local().cursor,
898 ansi::NamedColor::DimBlack => |theme| theme.colors().terminal_ansi_dim_black,
899 ansi::NamedColor::DimRed => |theme| theme.colors().terminal_ansi_dim_red,
900 ansi::NamedColor::DimGreen => |theme| theme.colors().terminal_ansi_dim_green,
901 ansi::NamedColor::DimYellow => |theme| theme.colors().terminal_ansi_dim_yellow,
902 ansi::NamedColor::DimBlue => |theme| theme.colors().terminal_ansi_dim_blue,
903 ansi::NamedColor::DimMagenta => |theme| theme.colors().terminal_ansi_dim_magenta,
904 ansi::NamedColor::DimCyan => |theme| theme.colors().terminal_ansi_dim_cyan,
905 ansi::NamedColor::DimWhite => |theme| theme.colors().terminal_ansi_dim_white,
906 ansi::NamedColor::BrightForeground => |theme| theme.colors().terminal_bright_foreground,
907 ansi::NamedColor::DimForeground => |theme| theme.colors().terminal_dim_foreground,
908 },
909 // 'True' colors
910 ansi::Color::Spec(_) => |theme| theme.colors().editor_background,
911 // 8 bit, indexed colors
912 ansi::Color::Indexed(i) => {
913 match i {
914 // 0-15 are the same as the named colors above
915 0 => |theme| theme.colors().terminal_ansi_black,
916 1 => |theme| theme.colors().terminal_ansi_red,
917 2 => |theme| theme.colors().terminal_ansi_green,
918 3 => |theme| theme.colors().terminal_ansi_yellow,
919 4 => |theme| theme.colors().terminal_ansi_blue,
920 5 => |theme| theme.colors().terminal_ansi_magenta,
921 6 => |theme| theme.colors().terminal_ansi_cyan,
922 7 => |theme| theme.colors().terminal_ansi_white,
923 8 => |theme| theme.colors().terminal_ansi_bright_black,
924 9 => |theme| theme.colors().terminal_ansi_bright_red,
925 10 => |theme| theme.colors().terminal_ansi_bright_green,
926 11 => |theme| theme.colors().terminal_ansi_bright_yellow,
927 12 => |theme| theme.colors().terminal_ansi_bright_blue,
928 13 => |theme| theme.colors().terminal_ansi_bright_magenta,
929 14 => |theme| theme.colors().terminal_ansi_bright_cyan,
930 15 => |theme| theme.colors().terminal_ansi_bright_white,
931 // 16-231 are a 6x6x6 RGB color cube, mapped to 0-255 using steps defined by XTerm.
932 // See: https://github.com/xterm-x11/xterm-snapshots/blob/master/256colres.pl
933 // 16..=231 => {
934 // let (r, g, b) = rgb_for_index(index as u8);
935 // rgba_color(
936 // if r == 0 { 0 } else { r * 40 + 55 },
937 // if g == 0 { 0 } else { g * 40 + 55 },
938 // if b == 0 { 0 } else { b * 40 + 55 },
939 // )
940 // }
941 // 232-255 are a 24-step grayscale ramp from (8, 8, 8) to (238, 238, 238).
942 // 232..=255 => {
943 // let i = index as u8 - 232; // Align index to 0..24
944 // let value = i * 10 + 8;
945 // rgba_color(value, value, value)
946 // }
947 // For compatibility with the alacritty::Colors interface
948 // See: https://github.com/alacritty/alacritty/blob/master/alacritty_terminal/src/term/color.rs
949 _ => |_| gpui::black(),
950 }
951 }
952 };
953 color_fetcher
954}
955
956#[cfg(test)]
957mod tests {
958 use super::*;
959 use crate::tests::init_test;
960 use editor::test::editor_test_context::EditorTestContext;
961 use gpui::TestAppContext;
962 use language::Point;
963
964 #[track_caller]
965 fn assert_completion_range(
966 input: &str,
967 expect: &str,
968 replacement: &str,
969 cx: &mut EditorTestContext,
970 ) {
971 cx.set_state(input);
972
973 let buffer_position = cx.editor(|editor, _, cx| {
974 editor
975 .selections
976 .newest::<Point>(&editor.display_snapshot(cx))
977 .start
978 });
979
980 let snapshot = &cx.buffer_snapshot();
981
982 let replace_range = ConsoleQueryBarCompletionProvider::replace_range_for_completion(
983 &cx.buffer_text(),
984 snapshot.anchor_before(buffer_position),
985 replacement.as_bytes(),
986 snapshot,
987 );
988
989 cx.update_editor(|editor, _, cx| {
990 editor.edit(
991 vec![(
992 snapshot.offset_for_anchor(&replace_range.start)
993 ..snapshot.offset_for_anchor(&replace_range.end),
994 replacement,
995 )],
996 cx,
997 );
998 });
999
1000 pretty_assertions::assert_eq!(expect, cx.display_text());
1001 }
1002
1003 #[gpui::test]
1004 async fn test_determine_completion_replace_range(cx: &mut TestAppContext) {
1005 init_test(cx);
1006
1007 let mut cx = EditorTestContext::new(cx).await;
1008
1009 assert_completion_range("resˇ", "result", "result", &mut cx);
1010 assert_completion_range("print(resˇ)", "print(result)", "result", &mut cx);
1011 assert_completion_range("$author->nˇ", "$author->name", "$author->name", &mut cx);
1012 assert_completion_range(
1013 "$author->books[ˇ",
1014 "$author->books[0]",
1015 "$author->books[0]",
1016 &mut cx,
1017 );
1018 }
1019}