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, |session, cx| {
145 session
146 .evaluate(
147 expression,
148 Some(dap::EvaluateArgumentsContext::Variables),
149 self.stack_frame_list.read(cx).selected_stack_frame_id(),
150 None,
151 cx,
152 )
153 .detach();
154 });
155 }
156
157 fn render_console(&self, cx: &Context<Self>) -> impl IntoElement {
158 let settings = ThemeSettings::get_global(cx);
159 let text_style = TextStyle {
160 color: if self.console.read(cx).read_only(cx) {
161 cx.theme().colors().text_disabled
162 } else {
163 cx.theme().colors().text
164 },
165 font_family: settings.buffer_font.family.clone(),
166 font_features: settings.buffer_font.features.clone(),
167 font_size: settings.buffer_font_size(cx).into(),
168 font_weight: settings.buffer_font.weight,
169 line_height: relative(settings.buffer_line_height.value()),
170 ..Default::default()
171 };
172
173 EditorElement::new(
174 &self.console,
175 EditorStyle {
176 background: cx.theme().colors().editor_background,
177 local_player: cx.theme().players().local(),
178 text: text_style,
179 ..Default::default()
180 },
181 )
182 }
183
184 fn render_query_bar(&self, cx: &Context<Self>) -> impl IntoElement {
185 let settings = ThemeSettings::get_global(cx);
186 let text_style = TextStyle {
187 color: if self.console.read(cx).read_only(cx) {
188 cx.theme().colors().text_disabled
189 } else {
190 cx.theme().colors().text
191 },
192 font_family: settings.ui_font.family.clone(),
193 font_features: settings.ui_font.features.clone(),
194 font_fallbacks: settings.ui_font.fallbacks.clone(),
195 font_size: TextSize::Editor.rems(cx).into(),
196 font_weight: settings.ui_font.weight,
197 line_height: relative(1.3),
198 ..Default::default()
199 };
200
201 EditorElement::new(
202 &self.query_bar,
203 EditorStyle {
204 background: cx.theme().colors().editor_background,
205 local_player: cx.theme().players().local(),
206 text: text_style,
207 ..Default::default()
208 },
209 )
210 }
211}
212
213impl Render for Console {
214 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
215 let session = self.session.clone();
216 let token = self.last_token;
217 self.update_output_task = cx.spawn_in(window, async move |this, cx| {
218 _ = session.update_in(cx, move |session, window, cx| {
219 let (output, last_processed_token) = session.output(token);
220
221 _ = this.update(cx, |this, cx| {
222 if last_processed_token == this.last_token {
223 return;
224 }
225 this.add_messages(output, window, cx);
226
227 this.last_token = last_processed_token;
228 });
229 });
230 });
231
232 v_flex()
233 .key_context("DebugConsole")
234 .on_action(cx.listener(Self::evaluate))
235 .size_full()
236 .child(self.render_console(cx))
237 .when(self.is_local(cx), |this| {
238 this.child(Divider::horizontal())
239 .child(self.render_query_bar(cx))
240 })
241 .border_2()
242 }
243}
244
245struct ConsoleQueryBarCompletionProvider(WeakEntity<Console>);
246
247impl CompletionProvider for ConsoleQueryBarCompletionProvider {
248 fn completions(
249 &self,
250 _excerpt_id: ExcerptId,
251 buffer: &Entity<Buffer>,
252 buffer_position: language::Anchor,
253 _trigger: editor::CompletionContext,
254 _window: &mut Window,
255 cx: &mut Context<Editor>,
256 ) -> Task<Result<Option<Vec<Completion>>>> {
257 let Some(console) = self.0.upgrade() else {
258 return Task::ready(Ok(None));
259 };
260
261 let support_completions = console
262 .read(cx)
263 .session
264 .read(cx)
265 .capabilities()
266 .supports_completions_request
267 .unwrap_or_default();
268
269 if support_completions {
270 self.client_completions(&console, buffer, buffer_position, cx)
271 } else {
272 self.variable_list_completions(&console, buffer, buffer_position, cx)
273 }
274 }
275
276 fn resolve_completions(
277 &self,
278 _buffer: Entity<Buffer>,
279 _completion_indices: Vec<usize>,
280 _completions: Rc<RefCell<Box<[Completion]>>>,
281 _cx: &mut Context<Editor>,
282 ) -> gpui::Task<gpui::Result<bool>> {
283 Task::ready(Ok(false))
284 }
285
286 fn apply_additional_edits_for_completion(
287 &self,
288 _buffer: Entity<Buffer>,
289 _completions: Rc<RefCell<Box<[Completion]>>>,
290 _completion_index: usize,
291 _push_to_history: bool,
292 _cx: &mut Context<Editor>,
293 ) -> gpui::Task<gpui::Result<Option<language::Transaction>>> {
294 Task::ready(Ok(None))
295 }
296
297 fn is_completion_trigger(
298 &self,
299 _buffer: &Entity<Buffer>,
300 _position: language::Anchor,
301 _text: &str,
302 _trigger_in_words: bool,
303 _cx: &mut Context<Editor>,
304 ) -> bool {
305 true
306 }
307}
308
309impl ConsoleQueryBarCompletionProvider {
310 fn variable_list_completions(
311 &self,
312 console: &Entity<Console>,
313 buffer: &Entity<Buffer>,
314 buffer_position: language::Anchor,
315 cx: &mut Context<Editor>,
316 ) -> Task<Result<Option<Vec<Completion>>>> {
317 let (variables, string_matches) = console.update(cx, |console, cx| {
318 let mut variables = HashMap::default();
319 let mut string_matches = Vec::default();
320
321 for variable in console.variable_list.update(cx, |variable_list, cx| {
322 variable_list.completion_variables(cx)
323 }) {
324 if let Some(evaluate_name) = &variable.evaluate_name {
325 variables.insert(evaluate_name.clone(), variable.value.clone());
326 string_matches.push(StringMatchCandidate {
327 id: 0,
328 string: evaluate_name.clone(),
329 char_bag: evaluate_name.chars().collect(),
330 });
331 }
332
333 variables.insert(variable.name.clone(), variable.value.clone());
334
335 string_matches.push(StringMatchCandidate {
336 id: 0,
337 string: variable.name.clone(),
338 char_bag: variable.name.chars().collect(),
339 });
340 }
341
342 (variables, string_matches)
343 });
344
345 let query = buffer.read(cx).text();
346
347 cx.spawn(async move |_, cx| {
348 let matches = fuzzy::match_strings(
349 &string_matches,
350 &query,
351 true,
352 10,
353 &Default::default(),
354 cx.background_executor().clone(),
355 )
356 .await;
357
358 Ok(Some(
359 matches
360 .iter()
361 .filter_map(|string_match| {
362 let variable_value = variables.get(&string_match.string)?;
363
364 Some(project::Completion {
365 replace_range: buffer_position..buffer_position,
366 new_text: string_match.string.clone(),
367 label: CodeLabel {
368 filter_range: 0..string_match.string.len(),
369 text: format!("{} {}", string_match.string.clone(), variable_value),
370 runs: Vec::new(),
371 },
372 icon_path: None,
373 documentation: None,
374 confirm: None,
375 source: project::CompletionSource::Custom,
376 insert_text_mode: None,
377 })
378 })
379 .collect(),
380 ))
381 })
382 }
383
384 fn client_completions(
385 &self,
386 console: &Entity<Console>,
387 buffer: &Entity<Buffer>,
388 buffer_position: language::Anchor,
389 cx: &mut Context<Editor>,
390 ) -> Task<Result<Option<Vec<Completion>>>> {
391 let completion_task = console.update(cx, |console, cx| {
392 console.session.update(cx, |state, cx| {
393 let frame_id = console.stack_frame_list.read(cx).selected_stack_frame_id();
394
395 state.completions(
396 CompletionsQuery::new(buffer.read(cx), buffer_position, frame_id),
397 cx,
398 )
399 })
400 });
401 let snapshot = buffer.read(cx).text_snapshot();
402 cx.background_executor().spawn(async move {
403 let completions = completion_task.await?;
404
405 Ok(Some(
406 completions
407 .into_iter()
408 .map(|completion| {
409 let new_text = completion
410 .text
411 .as_ref()
412 .unwrap_or(&completion.label)
413 .to_owned();
414 let mut word_bytes_length = 0;
415 for chunk in snapshot
416 .reversed_chunks_in_range(language::Anchor::MIN..buffer_position)
417 {
418 let mut processed_bytes = 0;
419 if let Some(_) = chunk.chars().rfind(|c| {
420 let is_whitespace = c.is_whitespace();
421 if !is_whitespace {
422 processed_bytes += c.len_utf8();
423 }
424
425 is_whitespace
426 }) {
427 word_bytes_length += processed_bytes;
428 break;
429 } else {
430 word_bytes_length += chunk.len();
431 }
432 }
433
434 let buffer_offset = buffer_position.to_offset(&snapshot);
435 let start = buffer_offset - word_bytes_length;
436 let start = snapshot.anchor_before(start);
437 let replace_range = start..buffer_position;
438
439 project::Completion {
440 replace_range,
441 new_text,
442 label: CodeLabel {
443 filter_range: 0..completion.label.len(),
444 text: completion.label,
445 runs: Vec::new(),
446 },
447 icon_path: None,
448 documentation: None,
449 confirm: None,
450 source: project::CompletionSource::BufferWord {
451 word_range: buffer_position..language::Anchor::MAX,
452 resolved: false,
453 },
454 insert_text_mode: None,
455 }
456 })
457 .collect(),
458 ))
459 })
460 }
461}