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 })
239 .border_2()
240 }
241}
242
243struct ConsoleQueryBarCompletionProvider(WeakEntity<Console>);
244
245impl CompletionProvider for ConsoleQueryBarCompletionProvider {
246 fn completions(
247 &self,
248 _excerpt_id: ExcerptId,
249 buffer: &Entity<Buffer>,
250 buffer_position: language::Anchor,
251 _trigger: editor::CompletionContext,
252 _window: &mut Window,
253 cx: &mut Context<Editor>,
254 ) -> Task<Result<Option<Vec<Completion>>>> {
255 let Some(console) = self.0.upgrade() else {
256 return Task::ready(Ok(None));
257 };
258
259 let support_completions = console
260 .read(cx)
261 .session
262 .read(cx)
263 .capabilities()
264 .supports_completions_request
265 .unwrap_or_default();
266
267 if support_completions {
268 self.client_completions(&console, buffer, buffer_position, cx)
269 } else {
270 self.variable_list_completions(&console, buffer, buffer_position, cx)
271 }
272 }
273
274 fn resolve_completions(
275 &self,
276 _buffer: Entity<Buffer>,
277 _completion_indices: Vec<usize>,
278 _completions: Rc<RefCell<Box<[Completion]>>>,
279 _cx: &mut Context<Editor>,
280 ) -> gpui::Task<gpui::Result<bool>> {
281 Task::ready(Ok(false))
282 }
283
284 fn apply_additional_edits_for_completion(
285 &self,
286 _buffer: Entity<Buffer>,
287 _completions: Rc<RefCell<Box<[Completion]>>>,
288 _completion_index: usize,
289 _push_to_history: bool,
290 _cx: &mut Context<Editor>,
291 ) -> gpui::Task<gpui::Result<Option<language::Transaction>>> {
292 Task::ready(Ok(None))
293 }
294
295 fn is_completion_trigger(
296 &self,
297 _buffer: &Entity<Buffer>,
298 _position: language::Anchor,
299 _text: &str,
300 _trigger_in_words: bool,
301 _cx: &mut Context<Editor>,
302 ) -> bool {
303 true
304 }
305}
306
307impl ConsoleQueryBarCompletionProvider {
308 fn variable_list_completions(
309 &self,
310 console: &Entity<Console>,
311 buffer: &Entity<Buffer>,
312 buffer_position: language::Anchor,
313 cx: &mut Context<Editor>,
314 ) -> Task<Result<Option<Vec<Completion>>>> {
315 let (variables, string_matches) = console.update(cx, |console, cx| {
316 let mut variables = HashMap::default();
317 let mut string_matches = Vec::default();
318
319 for variable in console.variable_list.update(cx, |variable_list, cx| {
320 variable_list.completion_variables(cx)
321 }) {
322 if let Some(evaluate_name) = &variable.evaluate_name {
323 variables.insert(evaluate_name.clone(), variable.value.clone());
324 string_matches.push(StringMatchCandidate {
325 id: 0,
326 string: evaluate_name.clone(),
327 char_bag: evaluate_name.chars().collect(),
328 });
329 }
330
331 variables.insert(variable.name.clone(), variable.value.clone());
332
333 string_matches.push(StringMatchCandidate {
334 id: 0,
335 string: variable.name.clone(),
336 char_bag: variable.name.chars().collect(),
337 });
338 }
339
340 (variables, string_matches)
341 });
342
343 let query = buffer.read(cx).text();
344
345 cx.spawn(async move |_, cx| {
346 let matches = fuzzy::match_strings(
347 &string_matches,
348 &query,
349 true,
350 10,
351 &Default::default(),
352 cx.background_executor().clone(),
353 )
354 .await;
355
356 Ok(Some(
357 matches
358 .iter()
359 .filter_map(|string_match| {
360 let variable_value = variables.get(&string_match.string)?;
361
362 Some(project::Completion {
363 replace_range: buffer_position..buffer_position,
364 new_text: string_match.string.clone(),
365 label: CodeLabel {
366 filter_range: 0..string_match.string.len(),
367 text: format!("{} {}", string_match.string.clone(), variable_value),
368 runs: Vec::new(),
369 },
370 icon_path: None,
371 documentation: None,
372 confirm: None,
373 source: project::CompletionSource::Custom,
374 insert_text_mode: None,
375 })
376 })
377 .collect(),
378 ))
379 })
380 }
381
382 fn client_completions(
383 &self,
384 console: &Entity<Console>,
385 buffer: &Entity<Buffer>,
386 buffer_position: language::Anchor,
387 cx: &mut Context<Editor>,
388 ) -> Task<Result<Option<Vec<Completion>>>> {
389 let completion_task = console.update(cx, |console, cx| {
390 console.session.update(cx, |state, cx| {
391 let frame_id = console.stack_frame_list.read(cx).selected_stack_frame_id();
392
393 state.completions(
394 CompletionsQuery::new(buffer.read(cx), buffer_position, frame_id),
395 cx,
396 )
397 })
398 });
399 let snapshot = buffer.read(cx).text_snapshot();
400 cx.background_executor().spawn(async move {
401 let completions = completion_task.await?;
402
403 Ok(Some(
404 completions
405 .into_iter()
406 .map(|completion| {
407 let new_text = completion
408 .text
409 .as_ref()
410 .unwrap_or(&completion.label)
411 .to_owned();
412 let mut word_bytes_length = 0;
413 for chunk in snapshot
414 .reversed_chunks_in_range(language::Anchor::MIN..buffer_position)
415 {
416 let mut processed_bytes = 0;
417 if let Some(_) = chunk.chars().rfind(|c| {
418 let is_whitespace = c.is_whitespace();
419 if !is_whitespace {
420 processed_bytes += c.len_utf8();
421 }
422
423 is_whitespace
424 }) {
425 word_bytes_length += processed_bytes;
426 break;
427 } else {
428 word_bytes_length += chunk.len();
429 }
430 }
431
432 let buffer_offset = buffer_position.to_offset(&snapshot);
433 let start = buffer_offset - word_bytes_length;
434 let start = snapshot.anchor_before(start);
435 let replace_range = start..buffer_position;
436
437 project::Completion {
438 replace_range,
439 new_text,
440 label: CodeLabel {
441 filter_range: 0..completion.label.len(),
442 text: completion.label,
443 runs: Vec::new(),
444 },
445 icon_path: None,
446 documentation: None,
447 confirm: None,
448 source: project::CompletionSource::BufferWord {
449 word_range: buffer_position..language::Anchor::MAX,
450 resolved: false,
451 },
452 insert_text_mode: None,
453 }
454 })
455 .collect(),
456 ))
457 })
458 }
459}