Skip to repository content880 lines · 32.2 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T05:39:49.598Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
console.rs
1use super::{
2 stack_frame_list::{StackFrameList, StackFrameListEvent},
3 variable_list::VariableList,
4};
5use anyhow::Result;
6use collections::HashMap;
7use dap::{CompletionItem, CompletionItemType, OutputEvent};
8use editor::{
9 Bias, CompletionProvider, Editor, EditorElement, EditorMode, EditorStyle, HighlightKey,
10 MultiBufferOffset, SizingBehavior,
11};
12use fuzzy::StringMatchCandidate;
13use gpui::{
14 Action as _, AppContext, Context, Entity, FocusHandle, Focusable, HighlightStyle, Hsla, Render,
15 Subscription, Task, TextStyle, WeakEntity, actions,
16};
17use language::{Anchor, Buffer, CharScopeContext, CodeLabel, TextBufferSnapshot, ToOffset};
18use menu::{Confirm, SelectNext, SelectPrevious};
19use project::{
20 CompletionDisplayOptions, CompletionResponse,
21 debugger::session::{CompletionsQuery, OutputToken, Session},
22 lsp_store::CompletionDocumentation,
23 search_history::{SearchHistory, SearchHistoryCursor},
24};
25use settings::Settings;
26use std::{ops::Range, rc::Rc, usize};
27use theme::Theme;
28use theme_settings::ThemeSettings;
29use ui::{ContextMenu, Divider, PopoverMenu, SplitButton, Tooltip, prelude::*};
30use util::ResultExt;
31
32actions!(
33 console,
34 [
35 /// Adds an expression to the watch list.
36 WatchExpression
37 ]
38);
39
40pub struct Console {
41 console: Entity<Editor>,
42 query_bar: Entity<Editor>,
43 session: Entity<Session>,
44 _subscriptions: Vec<Subscription>,
45 variable_list: Entity<VariableList>,
46 stack_frame_list: Entity<StackFrameList>,
47 last_token: OutputToken,
48 update_output_task: Option<Task<()>>,
49 focus_handle: FocusHandle,
50 history: SearchHistory,
51 cursor: SearchHistoryCursor,
52}
53
54impl Console {
55 pub fn new(
56 session: Entity<Session>,
57 stack_frame_list: Entity<StackFrameList>,
58 variable_list: Entity<VariableList>,
59 window: &mut Window,
60 cx: &mut Context<Self>,
61 ) -> Self {
62 let console = cx.new(|cx| {
63 let mut editor = Editor::multi_line(window, cx);
64 editor.set_mode(EditorMode::Full {
65 scale_ui_elements_with_buffer_font_size: true,
66 show_active_line_background: true,
67 sizing_behavior: SizingBehavior::ExcludeOverscrollMargin,
68 });
69 editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
70 editor.set_read_only(true);
71 editor.disable_scrollbars_and_minimap(window, cx);
72 editor.set_show_gutter(false, cx);
73 editor.set_show_runnables(false, cx);
74 editor.set_show_bookmarks(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.disable_mouse_wheel_zoom();
87 editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
88 editor
89 });
90 let focus_handle = cx.focus_handle();
91
92 let this = cx.weak_entity();
93 let query_bar = cx.new(|cx| {
94 let mut editor = Editor::single_line(window, cx);
95 editor.set_placeholder_text("Evaluate an expression", window, cx);
96 editor.set_use_autoclose(false);
97 editor.set_show_gutter(false, cx);
98 editor.set_show_wrap_guides(false, cx);
99 editor.set_show_indent_guides(false, cx);
100 editor.set_completion_provider(Some(Rc::new(ConsoleQueryBarCompletionProvider(this))));
101
102 editor
103 });
104
105 let _subscriptions = vec![
106 cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events),
107 cx.on_focus(&focus_handle, window, |console, window, cx| {
108 if console.is_running(cx) {
109 console.query_bar.focus_handle(cx).focus(window, cx);
110 }
111 }),
112 ];
113
114 Self {
115 session,
116 console,
117 query_bar,
118 variable_list,
119 _subscriptions,
120 stack_frame_list,
121 update_output_task: None,
122 last_token: OutputToken(0),
123 focus_handle,
124 history: SearchHistory::new(
125 None,
126 project::search_history::QueryInsertionBehavior::ReplacePreviousIfContains,
127 ),
128 cursor: Default::default(),
129 }
130 }
131
132 #[cfg(test)]
133 pub(crate) fn editor(&self) -> &Entity<Editor> {
134 &self.console
135 }
136
137 fn is_running(&self, cx: &Context<Self>) -> bool {
138 self.session.read(cx).is_started()
139 }
140
141 fn handle_stack_frame_list_events(
142 &mut self,
143 _: Entity<StackFrameList>,
144 event: &StackFrameListEvent,
145 cx: &mut Context<Self>,
146 ) {
147 match event {
148 StackFrameListEvent::SelectedStackFrameChanged(_) => cx.notify(),
149 StackFrameListEvent::BuiltEntries => {}
150 }
151 }
152
153 pub(crate) fn show_indicator(&self, cx: &App) -> bool {
154 self.session.read(cx).has_new_output(self.last_token)
155 }
156
157 fn add_messages(
158 &mut self,
159 events: Vec<OutputEvent>,
160 window: &mut Window,
161 cx: &mut App,
162 ) -> Task<Result<()>> {
163 self.console.update(cx, |_, cx| {
164 cx.spawn_in(window, async move |console, cx| {
165 let mut len = console
166 .update(cx, |this, cx| this.buffer().read(cx).len(cx))?
167 .0;
168 let (output, spans, background_spans) = cx
169 .background_spawn(async move {
170 let mut all_spans = Vec::new();
171 let mut all_background_spans = Vec::new();
172 let mut to_insert = String::new();
173 let mut scratch = String::new();
174
175 for event in &events {
176 scratch.clear();
177 let trimmed_output = event.output.trim_end();
178 scratch.push_str(trimmed_output);
179 scratch.push('\n');
180 let parsed_output = terminal::parse_ansi_text(scratch.as_bytes());
181 let output = parsed_output.text;
182 to_insert.extend(output.chars());
183 let mut spans = parsed_output.foreground_spans;
184 let mut background_spans = parsed_output.background_spans;
185
186 for (range, _) in spans.iter_mut() {
187 let start_offset = len + range.start;
188 *range = start_offset..len + range.end;
189 }
190
191 for (range, _) in background_spans.iter_mut() {
192 let start_offset = len + range.start;
193 *range = start_offset..len + range.end;
194 }
195
196 len += output.len();
197
198 all_spans.extend(spans);
199 all_background_spans.extend(background_spans);
200 }
201 (to_insert, all_spans, all_background_spans)
202 })
203 .await;
204 console.update_in(cx, |console, window, cx| {
205 console.set_read_only(false);
206 console.move_to_end(&editor::actions::MoveToEnd, window, cx);
207 console.insert(&output, window, cx);
208 console.set_read_only(true);
209
210 let buffer = console.buffer().read(cx).snapshot(cx);
211
212 for (range, color) in spans {
213 let Some(color) = color else { continue };
214 let start_offset = range.start;
215 let range = buffer.anchor_after(MultiBufferOffset(range.start))
216 ..buffer.anchor_before(MultiBufferOffset(range.end));
217 let style = HighlightStyle {
218 color: Some(terminal_view::terminal_element::convert_color(
219 &color,
220 cx.theme(),
221 )),
222 ..Default::default()
223 };
224 console.highlight_text_key(
225 HighlightKey::ConsoleAnsiHighlight(start_offset),
226 vec![range],
227 style,
228 false,
229 cx,
230 );
231 }
232
233 for (range, color) in background_spans {
234 let Some(color) = color else { continue };
235 let start_offset = range.start;
236 let range = buffer.anchor_after(MultiBufferOffset(range.start))
237 ..buffer.anchor_before(MultiBufferOffset(range.end));
238 let color_fn = background_color_fetcher(color);
239 console.highlight_background(
240 HighlightKey::ConsoleAnsiHighlight(start_offset),
241 &[range],
242 move |_, theme| color_fn(theme),
243 cx,
244 );
245 }
246
247 cx.notify();
248 })?;
249
250 Ok(())
251 })
252 })
253 }
254
255 pub fn watch_expression(
256 &mut self,
257 _: &WatchExpression,
258 window: &mut Window,
259 cx: &mut Context<Self>,
260 ) {
261 let expression = self.query_bar.update(cx, |editor, cx| {
262 let expression = editor.text(cx);
263 cx.defer_in(window, |editor, window, cx| {
264 editor.clear(window, cx);
265 });
266
267 expression
268 });
269 self.history.add(&mut self.cursor, expression.clone());
270 self.cursor.reset();
271 self.session.update(cx, |session, cx| {
272 session
273 .evaluate(
274 expression.clone(),
275 Some(dap::EvaluateArgumentsContext::Repl),
276 self.stack_frame_list.read(cx).opened_stack_frame_id(),
277 None,
278 cx,
279 )
280 .detach();
281
282 if let Some(stack_frame_id) = self.stack_frame_list.read(cx).opened_stack_frame_id() {
283 session
284 .add_watcher(expression.into(), stack_frame_id, cx)
285 .detach();
286 }
287 });
288 }
289
290 fn previous_query(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) {
291 let current_query = self.query_bar.read(cx).text(cx);
292 let prev = self.history.previous(&mut self.cursor, ¤t_query);
293 if let Some(prev) = prev {
294 self.query_bar.update(cx, |editor, cx| {
295 editor.set_text(prev, window, cx);
296 });
297 }
298 }
299
300 fn next_query(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
301 let next = self.history.next(&mut self.cursor);
302 let query = next.unwrap_or_else(|| {
303 self.cursor.reset();
304 ""
305 });
306
307 self.query_bar.update(cx, |editor, cx| {
308 editor.set_text(query, window, cx);
309 });
310 }
311
312 fn evaluate(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
313 let expression = self.query_bar.update(cx, |editor, cx| {
314 let expression = editor.text(cx);
315 cx.defer_in(window, |editor, window, cx| {
316 editor.clear(window, cx);
317 });
318
319 expression
320 });
321
322 self.history.add(&mut self.cursor, expression.clone());
323 self.cursor.reset();
324 self.session.update(cx, |session, cx| {
325 session
326 .evaluate(
327 expression,
328 Some(dap::EvaluateArgumentsContext::Repl),
329 self.stack_frame_list.read(cx).opened_stack_frame_id(),
330 None,
331 cx,
332 )
333 .detach();
334 });
335 }
336
337 fn render_submit_menu(
338 &self,
339 id: impl Into<ElementId>,
340 keybinding_target: Option<FocusHandle>,
341 cx: &App,
342 ) -> impl IntoElement {
343 PopoverMenu::new(id.into())
344 .trigger(
345 ui::ButtonLike::new_rounded_right("console-confirm-split-button-right")
346 .layer(ui::ElevationIndex::ModalSurface)
347 .size(ui::ButtonSize::None)
348 .child(
349 div()
350 .px_1()
351 .child(Icon::new(IconName::ChevronDown).size(IconSize::XSmall)),
352 ),
353 )
354 .when(
355 self.stack_frame_list
356 .read(cx)
357 .opened_stack_frame_id()
358 .is_some(),
359 |this| {
360 this.menu(move |window, cx| {
361 Some(ContextMenu::build(window, cx, |context_menu, _, _| {
362 context_menu
363 .when_some(keybinding_target.clone(), |el, keybinding_target| {
364 el.context(keybinding_target)
365 })
366 .action("Watch Expression", WatchExpression.boxed_clone())
367 }))
368 })
369 },
370 )
371 .anchor(gpui::Anchor::TopRight)
372 }
373
374 fn render_console(&self, cx: &Context<Self>) -> impl IntoElement {
375 EditorElement::new(&self.console, Self::editor_style(&self.console, cx))
376 }
377
378 fn editor_style(editor: &Entity<Editor>, cx: &Context<Self>) -> EditorStyle {
379 let is_read_only = editor.read(cx).read_only(cx);
380 let settings = ThemeSettings::get_global(cx);
381 let theme = cx.theme();
382 let text_style = TextStyle {
383 color: if is_read_only {
384 theme.colors().text_muted
385 } else {
386 theme.colors().text
387 },
388 font_family: settings.buffer_font.family.clone(),
389 font_features: settings.buffer_font.features.clone(),
390 font_size: settings.buffer_font_size(cx).into(),
391 font_weight: settings.buffer_font.weight,
392 line_height: relative(settings.buffer_line_height.value()),
393 ..Default::default()
394 };
395 EditorStyle {
396 background: theme.colors().editor_background,
397 local_player: theme.players().local(),
398 text: text_style,
399 ..Default::default()
400 }
401 }
402
403 fn render_query_bar(&self, cx: &Context<Self>) -> impl IntoElement {
404 EditorElement::new(&self.query_bar, Self::editor_style(&self.query_bar, cx))
405 }
406
407 pub(crate) fn update_output(&mut self, window: &mut Window, cx: &mut Context<Self>) {
408 if self.update_output_task.is_some() {
409 return;
410 }
411 let session = self.session.clone();
412 let token = self.last_token;
413 self.update_output_task = Some(cx.spawn_in(window, async move |this, cx| {
414 let Some((last_processed_token, task)) = session
415 .update_in(cx, |session, window, cx| {
416 let (output, last_processed_token) = session.output(token);
417
418 this.update(cx, |this, cx| {
419 if last_processed_token == this.last_token {
420 return None;
421 }
422 Some((
423 last_processed_token,
424 this.add_messages(output.cloned().collect(), window, cx),
425 ))
426 })
427 .ok()
428 .flatten()
429 })
430 .ok()
431 .flatten()
432 else {
433 _ = this.update(cx, |this, _| {
434 this.update_output_task.take();
435 });
436 return;
437 };
438 _ = task.await.log_err();
439 _ = this.update(cx, |this, _| {
440 this.last_token = last_processed_token;
441 this.update_output_task.take();
442 });
443 }));
444 }
445}
446
447impl Render for Console {
448 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
449 let query_focus_handle = self.query_bar.focus_handle(cx);
450 self.update_output(window, cx);
451
452 v_flex()
453 .track_focus(&self.focus_handle)
454 .key_context("DebugConsole")
455 .on_action(cx.listener(Self::evaluate))
456 .on_action(cx.listener(Self::watch_expression))
457 .size_full()
458 .border_2()
459 .bg(cx.theme().colors().editor_background)
460 .child(self.render_console(cx))
461 .when(self.is_running(cx), |this| {
462 this.child(Divider::horizontal()).child(
463 h_flex()
464 .on_action(cx.listener(Self::previous_query))
465 .on_action(cx.listener(Self::next_query))
466 .p_1()
467 .gap_1()
468 .bg(cx.theme().colors().editor_background)
469 .child(self.render_query_bar(cx))
470 .child(SplitButton::new(
471 ui::ButtonLike::new_rounded_all(ElementId::Name(
472 "split-button-left-confirm-button".into(),
473 ))
474 .on_click(move |_, window, cx| {
475 window.dispatch_action(Box::new(Confirm), cx)
476 })
477 .layer(ui::ElevationIndex::ModalSurface)
478 .size(ui::ButtonSize::Compact)
479 .child(Label::new("Evaluate"))
480 .tooltip({
481 let query_focus_handle = query_focus_handle.clone();
482
483 move |_window, cx| {
484 Tooltip::for_action_in(
485 "Evaluate",
486 &Confirm,
487 &query_focus_handle,
488 cx,
489 )
490 }
491 }),
492 self.render_submit_menu(
493 ElementId::Name("split-button-right-confirm-button".into()),
494 Some(query_focus_handle.clone()),
495 cx,
496 )
497 .into_any_element(),
498 )),
499 )
500 })
501 }
502}
503
504impl Focusable for Console {
505 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
506 self.focus_handle.clone()
507 }
508}
509
510struct ConsoleQueryBarCompletionProvider(WeakEntity<Console>);
511
512impl CompletionProvider for ConsoleQueryBarCompletionProvider {
513 fn completions(
514 &self,
515 buffer: &Entity<Buffer>,
516 buffer_position: language::Anchor,
517 _trigger: editor::CompletionContext,
518 _window: &mut Window,
519 cx: &mut Context<Editor>,
520 ) -> Task<Result<Vec<CompletionResponse>>> {
521 let Some(console) = self.0.upgrade() else {
522 return Task::ready(Ok(Vec::new()));
523 };
524
525 let support_completions = console
526 .read(cx)
527 .session
528 .read(cx)
529 .capabilities()
530 .supports_completions_request
531 .unwrap_or_default();
532
533 if support_completions {
534 self.client_completions(&console, buffer, buffer_position, cx)
535 } else {
536 self.variable_list_completions(&console, buffer, buffer_position, cx)
537 }
538 }
539
540 fn is_completion_trigger(
541 &self,
542 buffer: &Entity<Buffer>,
543 position: language::Anchor,
544 text: &str,
545 trigger_in_words: bool,
546 cx: &mut Context<Editor>,
547 ) -> bool {
548 let mut chars = text.chars();
549 let char = if let Some(char) = chars.next() {
550 char
551 } else {
552 return false;
553 };
554
555 let snapshot = buffer.read(cx).snapshot();
556
557 let classifier = snapshot
558 .char_classifier_at(position)
559 .scope_context(Some(CharScopeContext::Completion));
560 if trigger_in_words && classifier.is_word(char) {
561 return true;
562 }
563
564 self.0
565 .read_with(cx, |console, cx| {
566 console
567 .session
568 .read(cx)
569 .capabilities()
570 .completion_trigger_characters
571 .as_ref()
572 .map(|triggers| triggers.contains(&text.to_string()))
573 })
574 .ok()
575 .flatten()
576 .unwrap_or(true)
577 }
578}
579
580impl ConsoleQueryBarCompletionProvider {
581 fn variable_list_completions(
582 &self,
583 console: &Entity<Console>,
584 buffer: &Entity<Buffer>,
585 buffer_position: language::Anchor,
586 cx: &mut Context<Editor>,
587 ) -> Task<Result<Vec<CompletionResponse>>> {
588 let (variables, string_matches) = console.update(cx, |console, cx| {
589 let mut variables = HashMap::default();
590 let mut string_matches = Vec::default();
591
592 for variable in console.variable_list.update(cx, |variable_list, cx| {
593 variable_list.completion_variables(cx)
594 }) {
595 if let Some(evaluate_name) = &variable.evaluate_name
596 && variables
597 .insert(evaluate_name.clone(), variable.value.clone())
598 .is_none()
599 {
600 string_matches.push(StringMatchCandidate {
601 id: 0,
602 string: evaluate_name.clone(),
603 char_bag: evaluate_name.chars().collect(),
604 });
605 }
606
607 if variables
608 .insert(variable.name.clone(), variable.value.clone())
609 .is_none()
610 {
611 string_matches.push(StringMatchCandidate {
612 id: 0,
613 string: variable.name.clone(),
614 char_bag: variable.name.chars().collect(),
615 });
616 }
617 }
618
619 (variables, string_matches)
620 });
621
622 let snapshot = buffer.read(cx).text_snapshot();
623 let buffer_text = snapshot.text();
624
625 cx.spawn(async move |_, cx| {
626 const LIMIT: usize = 10;
627 let matches = fuzzy::match_strings(
628 &string_matches,
629 &buffer_text,
630 true,
631 true,
632 LIMIT,
633 &Default::default(),
634 cx.background_executor().clone(),
635 )
636 .await;
637
638 let completions = matches
639 .iter()
640 .filter_map(|string_match| {
641 let variable_value = variables.get(&string_match.string)?;
642
643 Some(project::Completion {
644 replace_range: Self::replace_range_for_completion(
645 &buffer_text,
646 buffer_position,
647 string_match.string.as_bytes(),
648 &snapshot,
649 ),
650 new_text: string_match.string.clone(),
651 label: CodeLabel::plain(string_match.string.clone(), None),
652 match_start: None,
653 snippet_deduplication_key: None,
654 icon_path: None,
655 icon_color: None,
656 documentation: Some(CompletionDocumentation::MultiLineMarkdown(
657 variable_value.into(),
658 )),
659 confirm: None,
660 source: project::CompletionSource::Custom,
661 insert_text_mode: None,
662 group: None,
663 })
664 })
665 .collect::<Vec<_>>();
666
667 Ok(vec![project::CompletionResponse {
668 is_incomplete: completions.len() >= LIMIT,
669 display_options: CompletionDisplayOptions::default(),
670 completions,
671 }])
672 })
673 }
674
675 fn replace_range_for_completion(
676 buffer_text: &String,
677 buffer_position: Anchor,
678 new_bytes: &[u8],
679 snapshot: &TextBufferSnapshot,
680 ) -> Range<Anchor> {
681 let buffer_offset = buffer_position.to_offset(snapshot);
682 let buffer_bytes = &buffer_text.as_bytes()[0..buffer_offset];
683
684 let mut prefix_len = 0;
685 for i in (0..new_bytes.len()).rev() {
686 if buffer_bytes.ends_with(&new_bytes[0..i]) {
687 prefix_len = i;
688 break;
689 }
690 }
691
692 let start = snapshot.clip_offset(buffer_offset - prefix_len, Bias::Left);
693
694 snapshot.anchor_before(start)..buffer_position
695 }
696
697 const fn completion_type_score(completion_type: CompletionItemType) -> usize {
698 match completion_type {
699 CompletionItemType::Field | CompletionItemType::Property => 0,
700 CompletionItemType::Variable | CompletionItemType::Value => 1,
701 CompletionItemType::Method
702 | CompletionItemType::Function
703 | CompletionItemType::Constructor => 2,
704 CompletionItemType::Class
705 | CompletionItemType::Interface
706 | CompletionItemType::Module => 3,
707 _ => 4,
708 }
709 }
710
711 fn completion_item_sort_text(completion_item: &CompletionItem) -> String {
712 completion_item.sort_text.clone().unwrap_or_else(|| {
713 format!(
714 "{:03}_{}",
715 Self::completion_type_score(
716 completion_item.type_.unwrap_or(CompletionItemType::Text)
717 ),
718 completion_item.label.to_ascii_lowercase()
719 )
720 })
721 }
722
723 fn client_completions(
724 &self,
725 console: &Entity<Console>,
726 buffer: &Entity<Buffer>,
727 buffer_position: language::Anchor,
728 cx: &mut Context<Editor>,
729 ) -> Task<Result<Vec<CompletionResponse>>> {
730 let completion_task = console.update(cx, |console, cx| {
731 console.session.update(cx, |state, cx| {
732 let frame_id = console.stack_frame_list.read(cx).opened_stack_frame_id();
733
734 state.completions(
735 CompletionsQuery::new(buffer.read(cx), buffer_position, frame_id),
736 cx,
737 )
738 })
739 });
740 let snapshot = buffer.read(cx).text_snapshot();
741 cx.background_executor().spawn(async move {
742 let completions = completion_task.await?;
743
744 let buffer_text = snapshot.text();
745
746 let completions = completions
747 .into_iter()
748 .map(|completion| {
749 let sort_text = Self::completion_item_sort_text(&completion);
750 let new_text = completion
751 .text
752 .as_ref()
753 .unwrap_or(&completion.label)
754 .to_owned();
755
756 project::Completion {
757 replace_range: Self::replace_range_for_completion(
758 &buffer_text,
759 buffer_position,
760 new_text.as_bytes(),
761 &snapshot,
762 ),
763 new_text,
764 label: CodeLabel::plain(completion.label, None),
765 icon_path: None,
766 icon_color: None,
767 documentation: completion.detail.map(|detail| {
768 CompletionDocumentation::MultiLineMarkdown(detail.into())
769 }),
770 match_start: None,
771 snippet_deduplication_key: None,
772 confirm: None,
773 source: project::CompletionSource::Dap { sort_text },
774 insert_text_mode: None,
775 group: None,
776 }
777 })
778 .collect();
779
780 Ok(vec![project::CompletionResponse {
781 completions,
782 display_options: CompletionDisplayOptions::default(),
783 is_incomplete: false,
784 }])
785 })
786 }
787}
788
789fn background_color_fetcher(color: terminal::Color) -> impl Fn(&Theme) -> Hsla {
790 move |theme| {
791 if terminal::is_default_background_color(color) {
792 theme.colors().terminal_background
793 } else {
794 terminal_view::terminal_element::convert_color(&color, theme)
795 }
796 }
797}
798
799#[cfg(test)]
800mod tests {
801 use super::*;
802 use crate::tests::init_test;
803 use editor::{MultiBufferOffset, test::editor_test_context::EditorTestContext};
804 use gpui::TestAppContext;
805 use language::Point;
806
807 #[track_caller]
808 fn assert_completion_range(
809 input: &str,
810 expect: &str,
811 replacement: &str,
812 cx: &mut EditorTestContext,
813 ) {
814 cx.set_state(input);
815
816 let buffer_position = cx.editor(|editor, _, cx| {
817 editor
818 .selections
819 .newest::<Point>(&editor.display_snapshot(cx))
820 .start
821 });
822
823 let snapshot = &cx.buffer_snapshot();
824
825 let replace_range = ConsoleQueryBarCompletionProvider::replace_range_for_completion(
826 &cx.buffer_text(),
827 snapshot.anchor_before(buffer_position),
828 replacement.as_bytes(),
829 snapshot,
830 );
831
832 cx.update_editor(|editor, _, cx| {
833 editor.edit(
834 vec![(
835 MultiBufferOffset(snapshot.offset_for_anchor(&replace_range.start))
836 ..MultiBufferOffset(snapshot.offset_for_anchor(&replace_range.end)),
837 replacement,
838 )],
839 cx,
840 );
841 });
842
843 pretty_assertions::assert_eq!(expect, cx.display_text());
844 }
845
846 #[gpui::test]
847 fn test_background_color_fetcher_preserves_default_background(cx: &mut TestAppContext) {
848 init_test(cx);
849
850 cx.update(|cx| {
851 let mut theme = theme::GlobalTheme::theme(cx).as_ref().clone();
852 theme.styles.colors.terminal_background = gpui::red();
853 theme.styles.colors.terminal_ansi_background = gpui::blue();
854
855 let color = background_color_fetcher(terminal::Color::Named(
856 terminal::NamedColor::Background,
857 ))(&theme);
858
859 assert_eq!(color, gpui::red());
860 });
861 }
862
863 #[gpui::test]
864 async fn test_determine_completion_replace_range(cx: &mut TestAppContext) {
865 init_test(cx);
866
867 let mut cx = EditorTestContext::new(cx).await;
868
869 assert_completion_range("resˇ", "result", "result", &mut cx);
870 assert_completion_range("print(resˇ)", "print(result)", "result", &mut cx);
871 assert_completion_range("$author->nˇ", "$author->name", "$author->name", &mut cx);
872 assert_completion_range(
873 "$author->books[ˇ",
874 "$author->books[0]",
875 "$author->books[0]",
876 &mut cx,
877 );
878 }
879}
880