Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T06:40:41.347Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

console.rs

880 lines · 32.2 KB · rust
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, &current_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
Served at tenant.openagents/omega Member data and write actions are omitted.