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