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