console.rs

  1use super::{
  2    stack_frame_list::{StackFrameList, StackFrameListEvent},
  3    variable_list::VariableList,
  4};
  5use anyhow::Result;
  6use collections::HashMap;
  7use dap::OutputEvent;
  8use editor::{Bias, CompletionProvider, Editor, EditorElement, EditorStyle, ExcerptId};
  9use fuzzy::StringMatchCandidate;
 10use gpui::{
 11    Context, Entity, FocusHandle, Focusable, Render, Subscription, Task, TextStyle, WeakEntity,
 12};
 13use language::{Buffer, CodeLabel, ToOffset};
 14use menu::Confirm;
 15use project::{
 16    Completion,
 17    debugger::session::{CompletionsQuery, OutputToken, Session},
 18};
 19use settings::Settings;
 20use std::{cell::RefCell, rc::Rc, usize};
 21use theme::ThemeSettings;
 22use ui::{Divider, prelude::*};
 23
 24pub struct Console {
 25    console: Entity<Editor>,
 26    query_bar: Entity<Editor>,
 27    session: Entity<Session>,
 28    _subscriptions: Vec<Subscription>,
 29    variable_list: Entity<VariableList>,
 30    stack_frame_list: Entity<StackFrameList>,
 31    last_token: OutputToken,
 32    update_output_task: Task<()>,
 33    focus_handle: FocusHandle,
 34}
 35
 36impl Console {
 37    pub fn new(
 38        session: Entity<Session>,
 39        stack_frame_list: Entity<StackFrameList>,
 40        variable_list: Entity<VariableList>,
 41        window: &mut Window,
 42        cx: &mut Context<Self>,
 43    ) -> Self {
 44        let console = cx.new(|cx| {
 45            let mut editor = Editor::multi_line(window, cx);
 46            editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
 47            editor.set_read_only(true);
 48            editor.disable_scrollbars_and_minimap(window, cx);
 49            editor.set_show_gutter(false, cx);
 50            editor.set_show_runnables(false, cx);
 51            editor.set_show_breakpoints(false, cx);
 52            editor.set_show_code_actions(false, cx);
 53            editor.set_show_line_numbers(false, cx);
 54            editor.set_show_git_diff_gutter(false, cx);
 55            editor.set_autoindent(false);
 56            editor.set_input_enabled(false);
 57            editor.set_use_autoclose(false);
 58            editor.set_show_wrap_guides(false, cx);
 59            editor.set_show_indent_guides(false, cx);
 60            editor.set_show_edit_predictions(Some(false), window, cx);
 61            editor.set_use_modal_editing(false);
 62            editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
 63            editor
 64        });
 65        let focus_handle = cx.focus_handle();
 66
 67        let this = cx.weak_entity();
 68        let query_bar = cx.new(|cx| {
 69            let mut editor = Editor::single_line(window, cx);
 70            editor.set_placeholder_text("Evaluate an expression", cx);
 71            editor.set_use_autoclose(false);
 72            editor.set_show_gutter(false, cx);
 73            editor.set_show_wrap_guides(false, cx);
 74            editor.set_show_indent_guides(false, cx);
 75            editor.set_completion_provider(Some(Box::new(ConsoleQueryBarCompletionProvider(this))));
 76
 77            editor
 78        });
 79
 80        let _subscriptions =
 81            vec![cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events)];
 82
 83        Self {
 84            session,
 85            console,
 86            query_bar,
 87            variable_list,
 88            _subscriptions,
 89            stack_frame_list,
 90            update_output_task: Task::ready(()),
 91            last_token: OutputToken(0),
 92            focus_handle,
 93        }
 94    }
 95
 96    #[cfg(test)]
 97    pub(crate) fn editor(&self) -> &Entity<Editor> {
 98        &self.console
 99    }
100
101    fn is_local(&self, cx: &Context<Self>) -> bool {
102        self.session.read(cx).is_local()
103    }
104
105    fn handle_stack_frame_list_events(
106        &mut self,
107        _: Entity<StackFrameList>,
108        event: &StackFrameListEvent,
109        cx: &mut Context<Self>,
110    ) {
111        match event {
112            StackFrameListEvent::SelectedStackFrameChanged(_) => cx.notify(),
113        }
114    }
115
116    pub(crate) fn show_indicator(&self, cx: &App) -> bool {
117        self.session.read(cx).has_new_output(self.last_token)
118    }
119
120    pub fn add_messages<'a>(
121        &mut self,
122        events: impl Iterator<Item = &'a OutputEvent>,
123        window: &mut Window,
124        cx: &mut App,
125    ) {
126        self.console.update(cx, |console, cx| {
127            let mut to_insert = String::default();
128            for event in events {
129                use std::fmt::Write;
130
131                _ = write!(to_insert, "{}\n", event.output.trim_end());
132            }
133
134            console.set_read_only(false);
135            console.move_to_end(&editor::actions::MoveToEnd, window, cx);
136            console.insert(&to_insert, window, cx);
137            console.set_read_only(true);
138
139            cx.notify();
140        });
141    }
142
143    pub fn evaluate(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
144        let expression = self.query_bar.update(cx, |editor, cx| {
145            let expression = editor.text(cx);
146
147            editor.clear(window, cx);
148
149            expression
150        });
151
152        self.session.update(cx, |session, cx| {
153            session
154                .evaluate(
155                    expression,
156                    Some(dap::EvaluateArgumentsContext::Repl),
157                    self.stack_frame_list.read(cx).selected_stack_frame_id(),
158                    None,
159                    cx,
160                )
161                .detach();
162        });
163    }
164
165    fn render_console(&self, cx: &Context<Self>) -> impl IntoElement {
166        EditorElement::new(&self.console, self.editor_style(cx))
167    }
168
169    fn editor_style(&self, cx: &Context<Self>) -> EditorStyle {
170        let settings = ThemeSettings::get_global(cx);
171        let text_style = TextStyle {
172            color: if self.console.read(cx).read_only(cx) {
173                cx.theme().colors().text_disabled
174            } else {
175                cx.theme().colors().text
176            },
177            font_family: settings.buffer_font.family.clone(),
178            font_features: settings.buffer_font.features.clone(),
179            font_size: settings.buffer_font_size(cx).into(),
180            font_weight: settings.buffer_font.weight,
181            line_height: relative(settings.buffer_line_height.value()),
182            ..Default::default()
183        };
184        EditorStyle {
185            background: cx.theme().colors().editor_background,
186            local_player: cx.theme().players().local(),
187            text: text_style,
188            ..Default::default()
189        }
190    }
191
192    fn render_query_bar(&self, cx: &Context<Self>) -> impl IntoElement {
193        EditorElement::new(&self.query_bar, self.editor_style(cx))
194    }
195}
196
197impl Render for Console {
198    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
199        let session = self.session.clone();
200        let token = self.last_token;
201        self.update_output_task = cx.spawn_in(window, async move |this, cx| {
202            _ = session.update_in(cx, move |session, window, cx| {
203                let (output, last_processed_token) = session.output(token);
204
205                _ = this.update(cx, |this, cx| {
206                    if last_processed_token == this.last_token {
207                        return;
208                    }
209                    this.add_messages(output, window, cx);
210
211                    this.last_token = last_processed_token;
212                });
213            });
214        });
215
216        v_flex()
217            .track_focus(&self.focus_handle)
218            .key_context("DebugConsole")
219            .on_action(cx.listener(Self::evaluate))
220            .size_full()
221            .child(self.render_console(cx))
222            .when(self.is_local(cx), |this| {
223                this.child(Divider::horizontal())
224                    .child(self.render_query_bar(cx))
225            })
226            .border_2()
227    }
228}
229
230impl Focusable for Console {
231    fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
232        self.focus_handle.clone()
233    }
234}
235
236struct ConsoleQueryBarCompletionProvider(WeakEntity<Console>);
237
238impl CompletionProvider for ConsoleQueryBarCompletionProvider {
239    fn completions(
240        &self,
241        _excerpt_id: ExcerptId,
242        buffer: &Entity<Buffer>,
243        buffer_position: language::Anchor,
244        _trigger: editor::CompletionContext,
245        _window: &mut Window,
246        cx: &mut Context<Editor>,
247    ) -> Task<Result<Option<Vec<Completion>>>> {
248        let Some(console) = self.0.upgrade() else {
249            return Task::ready(Ok(None));
250        };
251
252        let support_completions = console
253            .read(cx)
254            .session
255            .read(cx)
256            .capabilities()
257            .supports_completions_request
258            .unwrap_or_default();
259
260        if support_completions {
261            self.client_completions(&console, buffer, buffer_position, cx)
262        } else {
263            self.variable_list_completions(&console, buffer, buffer_position, cx)
264        }
265    }
266
267    fn resolve_completions(
268        &self,
269        _buffer: Entity<Buffer>,
270        _completion_indices: Vec<usize>,
271        _completions: Rc<RefCell<Box<[Completion]>>>,
272        _cx: &mut Context<Editor>,
273    ) -> gpui::Task<gpui::Result<bool>> {
274        Task::ready(Ok(false))
275    }
276
277    fn apply_additional_edits_for_completion(
278        &self,
279        _buffer: Entity<Buffer>,
280        _completions: Rc<RefCell<Box<[Completion]>>>,
281        _completion_index: usize,
282        _push_to_history: bool,
283        _cx: &mut Context<Editor>,
284    ) -> gpui::Task<gpui::Result<Option<language::Transaction>>> {
285        Task::ready(Ok(None))
286    }
287
288    fn is_completion_trigger(
289        &self,
290        _buffer: &Entity<Buffer>,
291        _position: language::Anchor,
292        _text: &str,
293        _trigger_in_words: bool,
294        _cx: &mut Context<Editor>,
295    ) -> bool {
296        true
297    }
298}
299
300impl ConsoleQueryBarCompletionProvider {
301    fn variable_list_completions(
302        &self,
303        console: &Entity<Console>,
304        buffer: &Entity<Buffer>,
305        buffer_position: language::Anchor,
306        cx: &mut Context<Editor>,
307    ) -> Task<Result<Option<Vec<Completion>>>> {
308        let (variables, string_matches) = console.update(cx, |console, cx| {
309            let mut variables = HashMap::default();
310            let mut string_matches = Vec::default();
311
312            for variable in console.variable_list.update(cx, |variable_list, cx| {
313                variable_list.completion_variables(cx)
314            }) {
315                if let Some(evaluate_name) = &variable.evaluate_name {
316                    variables.insert(evaluate_name.clone(), variable.value.clone());
317                    string_matches.push(StringMatchCandidate {
318                        id: 0,
319                        string: evaluate_name.clone(),
320                        char_bag: evaluate_name.chars().collect(),
321                    });
322                }
323
324                variables.insert(variable.name.clone(), variable.value.clone());
325
326                string_matches.push(StringMatchCandidate {
327                    id: 0,
328                    string: variable.name.clone(),
329                    char_bag: variable.name.chars().collect(),
330                });
331            }
332
333            (variables, string_matches)
334        });
335
336        let query = buffer.read(cx).text();
337
338        cx.spawn(async move |_, cx| {
339            let matches = fuzzy::match_strings(
340                &string_matches,
341                &query,
342                true,
343                10,
344                &Default::default(),
345                cx.background_executor().clone(),
346            )
347            .await;
348
349            Ok(Some(
350                matches
351                    .iter()
352                    .filter_map(|string_match| {
353                        let variable_value = variables.get(&string_match.string)?;
354
355                        Some(project::Completion {
356                            replace_range: buffer_position..buffer_position,
357                            new_text: string_match.string.clone(),
358                            label: CodeLabel {
359                                filter_range: 0..string_match.string.len(),
360                                text: format!("{} {}", string_match.string.clone(), variable_value),
361                                runs: Vec::new(),
362                            },
363                            icon_path: None,
364                            documentation: None,
365                            confirm: None,
366                            source: project::CompletionSource::Custom,
367                            insert_text_mode: None,
368                        })
369                    })
370                    .collect(),
371            ))
372        })
373    }
374
375    fn client_completions(
376        &self,
377        console: &Entity<Console>,
378        buffer: &Entity<Buffer>,
379        buffer_position: language::Anchor,
380        cx: &mut Context<Editor>,
381    ) -> Task<Result<Option<Vec<Completion>>>> {
382        let completion_task = console.update(cx, |console, cx| {
383            console.session.update(cx, |state, cx| {
384                let frame_id = console.stack_frame_list.read(cx).selected_stack_frame_id();
385
386                state.completions(
387                    CompletionsQuery::new(buffer.read(cx), buffer_position, frame_id),
388                    cx,
389                )
390            })
391        });
392        let snapshot = buffer.read(cx).text_snapshot();
393        cx.background_executor().spawn(async move {
394            let completions = completion_task.await?;
395
396            Ok(Some(
397                completions
398                    .into_iter()
399                    .map(|completion| {
400                        let new_text = completion
401                            .text
402                            .as_ref()
403                            .unwrap_or(&completion.label)
404                            .to_owned();
405                        let buffer_text = snapshot.text();
406                        let buffer_bytes = buffer_text.as_bytes();
407                        let new_bytes = new_text.as_bytes();
408
409                        let mut prefix_len = 0;
410                        for i in (0..new_bytes.len()).rev() {
411                            if buffer_bytes.ends_with(&new_bytes[0..i]) {
412                                prefix_len = i;
413                                break;
414                            }
415                        }
416
417                        let buffer_offset = buffer_position.to_offset(&snapshot);
418                        let start = buffer_offset - prefix_len;
419                        let start = snapshot.clip_offset(start, Bias::Left);
420                        let start = snapshot.anchor_before(start);
421                        let replace_range = start..buffer_position;
422
423                        project::Completion {
424                            replace_range,
425                            new_text,
426                            label: CodeLabel {
427                                filter_range: 0..completion.label.len(),
428                                text: completion.label,
429                                runs: Vec::new(),
430                            },
431                            icon_path: None,
432                            documentation: None,
433                            confirm: None,
434                            source: project::CompletionSource::BufferWord {
435                                word_range: buffer_position..language::Anchor::MAX,
436                                resolved: false,
437                            },
438                            insert_text_mode: None,
439                        }
440                    })
441                    .collect(),
442            ))
443        })
444    }
445}