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