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