1use super::{
2 stack_frame_list::{StackFrameList, StackFrameListEvent},
3 variable_list::VariableList,
4};
5use anyhow::Result;
6use collections::HashMap;
7use dap::OutputEvent;
8use editor::{Bias, 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, SessionEvent},
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.disable_scrollbars_and_minimap(window, cx);
49 editor.set_show_gutter(false, cx);
50 editor.set_show_runnables(false, cx);
51 editor.set_show_breakpoints(false, cx);
52 editor.set_show_code_actions(false, cx);
53 editor.set_show_line_numbers(false, cx);
54 editor.set_show_git_diff_gutter(false, cx);
55 editor.set_autoindent(false);
56 editor.set_input_enabled(false);
57 editor.set_use_autoclose(false);
58 editor.set_show_wrap_guides(false, cx);
59 editor.set_show_indent_guides(false, cx);
60 editor.set_show_edit_predictions(Some(false), window, cx);
61 editor.set_use_modal_editing(false);
62 editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
63 editor
64 });
65 let focus_handle = cx.focus_handle();
66
67 let this = cx.weak_entity();
68 let query_bar = cx.new(|cx| {
69 let mut editor = Editor::single_line(window, cx);
70 editor.set_placeholder_text("Evaluate an expression", cx);
71 editor.set_use_autoclose(false);
72 editor.set_show_gutter(false, cx);
73 editor.set_show_wrap_guides(false, cx);
74 editor.set_show_indent_guides(false, cx);
75 editor.set_completion_provider(Some(Rc::new(ConsoleQueryBarCompletionProvider(this))));
76
77 editor
78 });
79
80 let _subscriptions = vec![
81 cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events),
82 cx.subscribe_in(&session, window, |this, _, event, window, cx| {
83 if let SessionEvent::ConsoleOutput = event {
84 this.update_output(window, cx)
85 }
86 }),
87 cx.on_focus(&focus_handle, window, |console, window, cx| {
88 if console.is_running(cx) {
89 console.query_bar.focus_handle(cx).focus(window);
90 }
91 }),
92 ];
93
94 Self {
95 session,
96 console,
97 query_bar,
98 variable_list,
99 _subscriptions,
100 stack_frame_list,
101 update_output_task: Task::ready(()),
102 last_token: OutputToken(0),
103 focus_handle,
104 }
105 }
106
107 #[cfg(test)]
108 pub(crate) fn editor(&self) -> &Entity<Editor> {
109 &self.console
110 }
111
112 fn is_running(&self, cx: &Context<Self>) -> bool {
113 self.session.read(cx).is_running()
114 }
115
116 fn handle_stack_frame_list_events(
117 &mut self,
118 _: Entity<StackFrameList>,
119 event: &StackFrameListEvent,
120 cx: &mut Context<Self>,
121 ) {
122 match event {
123 StackFrameListEvent::SelectedStackFrameChanged(_) => cx.notify(),
124 StackFrameListEvent::BuiltEntries => {}
125 }
126 }
127
128 pub(crate) fn show_indicator(&self, cx: &App) -> bool {
129 self.session.read(cx).has_new_output(self.last_token)
130 }
131
132 pub fn add_messages<'a>(
133 &mut self,
134 events: impl Iterator<Item = &'a OutputEvent>,
135 window: &mut Window,
136 cx: &mut App,
137 ) {
138 self.console.update(cx, |console, cx| {
139 let mut to_insert = String::default();
140 for event in events {
141 use std::fmt::Write;
142
143 _ = write!(to_insert, "{}\n", event.output.trim_end());
144 }
145
146 console.set_read_only(false);
147 console.move_to_end(&editor::actions::MoveToEnd, window, cx);
148 console.insert(&to_insert, window, cx);
149 console.set_read_only(true);
150
151 cx.notify();
152 });
153 }
154
155 pub fn evaluate(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
156 let expression = self.query_bar.update(cx, |editor, cx| {
157 let expression = editor.text(cx);
158 cx.defer_in(window, |editor, window, cx| {
159 editor.clear(window, cx);
160 });
161
162 expression
163 });
164
165 self.session.update(cx, |session, cx| {
166 session
167 .evaluate(
168 expression,
169 Some(dap::EvaluateArgumentsContext::Repl),
170 self.stack_frame_list.read(cx).opened_stack_frame_id(),
171 None,
172 cx,
173 )
174 .detach();
175 });
176 }
177
178 fn render_console(&self, cx: &Context<Self>) -> impl IntoElement {
179 EditorElement::new(&self.console, self.editor_style(cx))
180 }
181
182 fn editor_style(&self, cx: &Context<Self>) -> EditorStyle {
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.buffer_font.family.clone(),
191 font_features: settings.buffer_font.features.clone(),
192 font_size: settings.buffer_font_size(cx).into(),
193 font_weight: settings.buffer_font.weight,
194 line_height: relative(settings.buffer_line_height.value()),
195 ..Default::default()
196 };
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 fn render_query_bar(&self, cx: &Context<Self>) -> impl IntoElement {
206 EditorElement::new(&self.query_bar, self.editor_style(cx))
207 }
208
209 fn update_output(&mut self, window: &mut Window, cx: &mut Context<Self>) {
210 let session = self.session.clone();
211 let token = self.last_token;
212
213 self.update_output_task = cx.spawn_in(window, async move |this, cx| {
214 _ = session.update_in(cx, move |session, window, cx| {
215 let (output, last_processed_token) = session.output(token);
216
217 _ = this.update(cx, |this, cx| {
218 if last_processed_token == this.last_token {
219 return;
220 }
221 this.add_messages(output, window, cx);
222
223 this.last_token = last_processed_token;
224 });
225 });
226 });
227 }
228}
229
230impl Render for Console {
231 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
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_running(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<anyhow::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<anyhow::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, 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).opened_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 buffer_text = snapshot.text();
422 let buffer_bytes = buffer_text.as_bytes();
423 let new_bytes = new_text.as_bytes();
424
425 let mut prefix_len = 0;
426 for i in (0..new_bytes.len()).rev() {
427 if buffer_bytes.ends_with(&new_bytes[0..i]) {
428 prefix_len = i;
429 break;
430 }
431 }
432
433 let buffer_offset = buffer_position.to_offset(&snapshot);
434 let start = buffer_offset - prefix_len;
435 let start = snapshot.clip_offset(start, Bias::Left);
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}