1use anyhow::bail;
2use collections::{FxHashMap, HashMap};
3use language::LanguageRegistry;
4use paths::local_debug_file_relative_path;
5use std::{
6 borrow::Cow,
7 path::{Path, PathBuf},
8 sync::Arc,
9 time::Duration,
10 usize,
11};
12use tasks_ui::{TaskOverrides, TasksModal};
13
14use dap::{
15 DapRegistry, DebugRequest, TelemetrySpawnLocation, adapters::DebugAdapterName, send_telemetry,
16};
17use editor::{Editor, EditorElement, EditorStyle};
18use fuzzy::{StringMatch, StringMatchCandidate};
19use gpui::{
20 Action, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
21 HighlightStyle, InteractiveText, KeyContext, PromptButton, PromptLevel, Render, StyledText,
22 Subscription, Task, TextStyle, UnderlineStyle, WeakEntity,
23};
24use itertools::Itertools as _;
25use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
26use project::{ProjectPath, TaskContexts, TaskSourceKind, task_store::TaskStore};
27use settings::{Settings, initial_local_debug_tasks_content};
28use task::{DebugScenario, RevealTarget, ZedDebugConfig};
29use theme::ThemeSettings;
30use ui::{
31 ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
32 ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconName, IconSize,
33 IconWithIndicator, Indicator, InteractiveElement, IntoElement, KeyBinding, Label,
34 LabelCommon as _, LabelSize, ListItem, ListItemSpacing, ParentElement, RenderOnce,
35 SharedString, Styled, StyledExt, StyledTypography, ToggleButton, ToggleState, Toggleable,
36 Tooltip, Window, div, h_flex, px, relative, rems, v_flex,
37};
38use util::ResultExt;
39use workspace::{ModalView, Workspace, pane};
40
41use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
42
43#[allow(unused)]
44enum SaveScenarioState {
45 Saving,
46 Saved((ProjectPath, SharedString)),
47 Failed(SharedString),
48}
49
50pub(super) struct NewProcessModal {
51 workspace: WeakEntity<Workspace>,
52 debug_panel: WeakEntity<DebugPanel>,
53 mode: NewProcessMode,
54 debug_picker: Entity<Picker<DebugDelegate>>,
55 attach_mode: Entity<AttachMode>,
56 configure_mode: Entity<ConfigureMode>,
57 task_mode: TaskMode,
58 debugger: Option<DebugAdapterName>,
59 save_scenario_state: Option<SaveScenarioState>,
60 _subscriptions: [Subscription; 3],
61}
62
63fn suggested_label(request: &DebugRequest, debugger: &str) -> SharedString {
64 match request {
65 DebugRequest::Launch(config) => {
66 let last_path_component = Path::new(&config.program)
67 .file_name()
68 .map(|name| name.to_string_lossy())
69 .unwrap_or_else(|| Cow::Borrowed(&config.program));
70
71 format!("{} ({debugger})", last_path_component).into()
72 }
73 DebugRequest::Attach(config) => format!(
74 "pid: {} ({debugger})",
75 config.process_id.unwrap_or(u32::MAX)
76 )
77 .into(),
78 }
79}
80
81impl NewProcessModal {
82 pub(super) fn show(
83 workspace: &mut Workspace,
84 window: &mut Window,
85 mode: NewProcessMode,
86 reveal_target: Option<RevealTarget>,
87 cx: &mut Context<Workspace>,
88 ) {
89 let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
90 return;
91 };
92 let task_store = workspace.project().read(cx).task_store().clone();
93 let languages = workspace.app_state().languages.clone();
94
95 cx.spawn_in(window, async move |workspace, cx| {
96 let task_contexts = workspace.update_in(cx, |workspace, window, cx| {
97 tasks_ui::task_contexts(workspace, window, cx)
98 })?;
99 workspace.update_in(cx, |workspace, window, cx| {
100 let workspace_handle = workspace.weak_handle();
101 workspace.toggle_modal(window, cx, |window, cx| {
102 let attach_mode = AttachMode::new(None, workspace_handle.clone(), window, cx);
103
104 let debug_picker = cx.new(|cx| {
105 let delegate =
106 DebugDelegate::new(debug_panel.downgrade(), task_store.clone());
107 Picker::uniform_list(delegate, window, cx).modal(false)
108 });
109
110 let configure_mode = ConfigureMode::new(window, cx);
111
112 let task_overrides = Some(TaskOverrides { reveal_target });
113
114 let task_mode = TaskMode {
115 task_modal: cx.new(|cx| {
116 TasksModal::new(
117 task_store.clone(),
118 Arc::new(TaskContexts::default()),
119 task_overrides,
120 false,
121 workspace_handle.clone(),
122 window,
123 cx,
124 )
125 }),
126 };
127
128 let _subscriptions = [
129 cx.subscribe(&debug_picker, |_, _, _, cx| {
130 cx.emit(DismissEvent);
131 }),
132 cx.subscribe(
133 &attach_mode.read(cx).attach_picker.clone(),
134 |_, _, _, cx| {
135 cx.emit(DismissEvent);
136 },
137 ),
138 cx.subscribe(&task_mode.task_modal, |_, _, _: &DismissEvent, cx| {
139 cx.emit(DismissEvent)
140 }),
141 ];
142
143 cx.spawn_in(window, {
144 let debug_picker = debug_picker.downgrade();
145 let configure_mode = configure_mode.downgrade();
146 let task_modal = task_mode.task_modal.downgrade();
147 let workspace = workspace_handle.clone();
148
149 async move |this, cx| {
150 let task_contexts = task_contexts.await;
151 let task_contexts = Arc::new(task_contexts);
152 let lsp_task_sources = task_contexts.lsp_task_sources.clone();
153 let task_position = task_contexts.latest_selection;
154 // Get LSP tasks and filter out based on language vs lsp preference
155 let (lsp_tasks, prefer_lsp) =
156 workspace.update(cx, |workspace, cx| {
157 let lsp_tasks = editor::lsp_tasks(
158 workspace.project().clone(),
159 &lsp_task_sources,
160 task_position,
161 cx,
162 );
163 let prefer_lsp = workspace
164 .active_item(cx)
165 .and_then(|item| item.downcast::<Editor>())
166 .map(|editor| {
167 editor
168 .read(cx)
169 .buffer()
170 .read(cx)
171 .language_settings(cx)
172 .tasks
173 .prefer_lsp
174 })
175 .unwrap_or(false);
176 (lsp_tasks, prefer_lsp)
177 })?;
178
179 let lsp_tasks = lsp_tasks.await;
180 let add_current_language_tasks = !prefer_lsp || lsp_tasks.is_empty();
181
182 let lsp_tasks = lsp_tasks
183 .into_iter()
184 .flat_map(|(kind, tasks_with_locations)| {
185 tasks_with_locations
186 .into_iter()
187 .sorted_by_key(|(location, task)| {
188 (location.is_none(), task.resolved_label.clone())
189 })
190 .map(move |(_, task)| (kind.clone(), task))
191 })
192 .collect::<Vec<_>>();
193
194 let Some(task_inventory) = task_store
195 .update(cx, |task_store, _| task_store.task_inventory().cloned())?
196 else {
197 return Ok(());
198 };
199
200 let (used_tasks, current_resolved_tasks) = task_inventory
201 .update(cx, |task_inventory, cx| {
202 task_inventory
203 .used_and_current_resolved_tasks(task_contexts.clone(), cx)
204 })?
205 .await;
206
207 if let Ok(task) = debug_picker.update(cx, |picker, cx| {
208 picker.delegate.tasks_loaded(
209 task_contexts.clone(),
210 languages,
211 lsp_tasks.clone(),
212 current_resolved_tasks.clone(),
213 add_current_language_tasks,
214 cx,
215 )
216 }) {
217 task.await;
218 debug_picker
219 .update_in(cx, |picker, window, cx| {
220 picker.refresh(window, cx);
221 cx.notify();
222 })
223 .ok();
224 }
225
226 if let Some(active_cwd) = task_contexts
227 .active_context()
228 .and_then(|context| context.cwd.clone())
229 {
230 configure_mode
231 .update_in(cx, |configure_mode, window, cx| {
232 configure_mode.load(active_cwd, window, cx);
233 })
234 .ok();
235 }
236
237 task_modal
238 .update_in(cx, |task_modal, window, cx| {
239 task_modal.tasks_loaded(
240 task_contexts,
241 lsp_tasks,
242 used_tasks,
243 current_resolved_tasks,
244 add_current_language_tasks,
245 window,
246 cx,
247 );
248 })
249 .ok();
250
251 this.update(cx, |_, cx| {
252 cx.notify();
253 })
254 .ok();
255
256 anyhow::Ok(())
257 }
258 })
259 .detach();
260
261 Self {
262 debug_picker,
263 attach_mode,
264 configure_mode,
265 task_mode,
266 debugger: None,
267 mode,
268 debug_panel: debug_panel.downgrade(),
269 workspace: workspace_handle,
270 save_scenario_state: None,
271 _subscriptions,
272 }
273 });
274 })?;
275
276 anyhow::Ok(())
277 })
278 .detach();
279 }
280
281 fn render_mode(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
282 let dap_menu = self.adapter_drop_down_menu(window, cx);
283 match self.mode {
284 NewProcessMode::Task => self
285 .task_mode
286 .task_modal
287 .read(cx)
288 .picker
289 .clone()
290 .into_any_element(),
291 NewProcessMode::Attach => self.attach_mode.update(cx, |this, cx| {
292 this.clone().render(window, cx).into_any_element()
293 }),
294 NewProcessMode::Launch => self.configure_mode.update(cx, |this, cx| {
295 this.clone().render(dap_menu, window, cx).into_any_element()
296 }),
297 NewProcessMode::Debug => v_flex()
298 .w(rems(34.))
299 .child(self.debug_picker.clone())
300 .into_any_element(),
301 }
302 }
303
304 fn mode_focus_handle(&self, cx: &App) -> FocusHandle {
305 match self.mode {
306 NewProcessMode::Task => self.task_mode.task_modal.focus_handle(cx),
307 NewProcessMode::Attach => self.attach_mode.read(cx).attach_picker.focus_handle(cx),
308 NewProcessMode::Launch => self.configure_mode.read(cx).program.focus_handle(cx),
309 NewProcessMode::Debug => self.debug_picker.focus_handle(cx),
310 }
311 }
312
313 fn debug_scenario(&self, debugger: &str, cx: &App) -> Task<Option<DebugScenario>> {
314 let request = match self.mode {
315 NewProcessMode::Launch => {
316 DebugRequest::Launch(self.configure_mode.read(cx).debug_request(cx))
317 }
318 NewProcessMode::Attach => {
319 DebugRequest::Attach(self.attach_mode.read(cx).debug_request())
320 }
321 _ => return Task::ready(None),
322 };
323 let label = suggested_label(&request, debugger);
324
325 let stop_on_entry = if let NewProcessMode::Launch = &self.mode {
326 Some(self.configure_mode.read(cx).stop_on_entry.selected())
327 } else {
328 None
329 };
330
331 let session_scenario = ZedDebugConfig {
332 adapter: debugger.to_owned().into(),
333 label,
334 request,
335 stop_on_entry,
336 };
337
338 let adapter = cx
339 .global::<DapRegistry>()
340 .adapter(&session_scenario.adapter);
341
342 cx.spawn(async move |_| adapter?.config_from_zed_format(session_scenario).await.ok())
343 }
344
345 fn start_new_session(&mut self, window: &mut Window, cx: &mut Context<Self>) {
346 if self.debugger.as_ref().is_none() {
347 return;
348 }
349
350 if let NewProcessMode::Debug = &self.mode {
351 self.debug_picker.update(cx, |picker, cx| {
352 picker.delegate.confirm(false, window, cx);
353 });
354 return;
355 }
356
357 if let NewProcessMode::Launch = &self.mode {
358 if self.configure_mode.read(cx).save_to_debug_json.selected() {
359 self.save_debug_scenario(window, cx);
360 }
361 }
362
363 let Some(debugger) = self.debugger.clone() else {
364 return;
365 };
366
367 let debug_panel = self.debug_panel.clone();
368 let Some(task_contexts) = self.task_contexts(cx) else {
369 return;
370 };
371
372 let task_context = task_contexts.active_context().cloned().unwrap_or_default();
373 let worktree_id = task_contexts.worktree();
374 let mode = self.mode;
375 cx.spawn_in(window, async move |this, cx| {
376 let Some(config) = this
377 .update(cx, |this, cx| this.debug_scenario(&debugger, cx))?
378 .await
379 else {
380 bail!("debug config not found in mode: {mode}");
381 };
382
383 debug_panel.update_in(cx, |debug_panel, window, cx| {
384 send_telemetry(&config, TelemetrySpawnLocation::Custom, cx);
385 debug_panel.start_session(config, task_context, None, worktree_id, window, cx)
386 })?;
387 this.update(cx, |_, cx| {
388 cx.emit(DismissEvent);
389 })
390 .ok();
391 anyhow::Ok(())
392 })
393 .detach_and_log_err(cx);
394 }
395
396 fn update_attach_picker(
397 attach: &Entity<AttachMode>,
398 adapter: &DebugAdapterName,
399 window: &mut Window,
400 cx: &mut App,
401 ) {
402 attach.update(cx, |this, cx| {
403 if adapter.0 != this.definition.adapter {
404 this.definition.adapter = adapter.0.clone();
405
406 this.attach_picker.update(cx, |this, cx| {
407 this.picker.update(cx, |this, cx| {
408 this.delegate.definition.adapter = adapter.0.clone();
409 this.focus(window, cx);
410 })
411 });
412 }
413
414 cx.notify();
415 })
416 }
417
418 fn task_contexts(&self, cx: &App) -> Option<Arc<TaskContexts>> {
419 self.debug_picker.read(cx).delegate.task_contexts.clone()
420 }
421
422 fn save_debug_scenario(&mut self, window: &mut Window, cx: &mut Context<Self>) {
423 let task_contents = self.task_contexts(cx);
424 let Some(adapter) = self.debugger.as_ref() else {
425 return;
426 };
427 let scenario = self.debug_scenario(&adapter, cx);
428
429 self.save_scenario_state = Some(SaveScenarioState::Saving);
430
431 cx.spawn_in(window, async move |this, cx| {
432 let Some((scenario, worktree_id)) = scenario
433 .await
434 .zip(task_contents.and_then(|tcx| tcx.worktree()))
435 else {
436 this.update(cx, |this, _| {
437 this.save_scenario_state = Some(SaveScenarioState::Failed(
438 "Couldn't get scenario or task contents".into(),
439 ))
440 })
441 .ok();
442 return;
443 };
444
445 let Some(save_scenario) = this
446 .update_in(cx, |this, window, cx| {
447 this.debug_panel
448 .update(cx, |panel, cx| {
449 panel.save_scenario(&scenario, worktree_id, window, cx)
450 })
451 .ok()
452 })
453 .ok()
454 .flatten()
455 else {
456 return;
457 };
458 let res = save_scenario.await;
459
460 this.update(cx, |this, _| match res {
461 Ok(saved_file) => {
462 this.save_scenario_state = Some(SaveScenarioState::Saved((
463 saved_file,
464 scenario.label.clone(),
465 )))
466 }
467 Err(error) => {
468 this.save_scenario_state =
469 Some(SaveScenarioState::Failed(error.to_string().into()))
470 }
471 })
472 .ok();
473
474 cx.background_executor().timer(Duration::from_secs(3)).await;
475 this.update(cx, |this, _| this.save_scenario_state.take())
476 .ok();
477 })
478 .detach();
479 }
480
481 fn adapter_drop_down_menu(
482 &mut self,
483 window: &mut Window,
484 cx: &mut Context<Self>,
485 ) -> ui::DropdownMenu {
486 let workspace = self.workspace.clone();
487 let weak = cx.weak_entity();
488 let active_buffer = self.task_contexts(cx).and_then(|tc| {
489 tc.active_item_context
490 .as_ref()
491 .and_then(|aic| aic.1.as_ref().map(|l| l.buffer.clone()))
492 });
493
494 let active_buffer_language = active_buffer
495 .and_then(|buffer| buffer.read(cx).language())
496 .cloned();
497
498 let mut available_adapters = workspace
499 .update(cx, |_, cx| DapRegistry::global(cx).enumerate_adapters())
500 .unwrap_or_default();
501 if let Some(language) = active_buffer_language {
502 available_adapters.sort_by_key(|adapter| {
503 language
504 .config()
505 .debuggers
506 .get_index_of(adapter.0.as_ref())
507 .unwrap_or(usize::MAX)
508 });
509 if self.debugger.is_none() {
510 self.debugger = available_adapters.first().cloned();
511 }
512 }
513
514 let label = self
515 .debugger
516 .as_ref()
517 .map(|d| d.0.clone())
518 .unwrap_or_else(|| SELECT_DEBUGGER_LABEL.clone());
519
520 DropdownMenu::new(
521 "dap-adapter-picker",
522 label,
523 ContextMenu::build(window, cx, move |mut menu, _, _| {
524 let setter_for_name = |name: DebugAdapterName| {
525 let weak = weak.clone();
526 move |window: &mut Window, cx: &mut App| {
527 weak.update(cx, |this, cx| {
528 this.debugger = Some(name.clone());
529 cx.notify();
530 if let NewProcessMode::Attach = &this.mode {
531 Self::update_attach_picker(&this.attach_mode, &name, window, cx);
532 }
533 })
534 .ok();
535 }
536 };
537
538 for adapter in available_adapters.into_iter() {
539 menu = menu.entry(adapter.0.clone(), None, setter_for_name(adapter.clone()));
540 }
541
542 menu
543 }),
544 )
545 }
546
547 fn open_debug_json(&self, window: &mut Window, cx: &mut Context<NewProcessModal>) {
548 let this = cx.entity();
549 window
550 .spawn(cx, async move |cx| {
551 let worktree_id = this.update(cx, |this, cx| {
552 let tcx = this.task_contexts(cx);
553 tcx?.worktree()
554 })?;
555
556 let Some(worktree_id) = worktree_id else {
557 let _ = cx.prompt(
558 PromptLevel::Critical,
559 "Cannot open debug.json",
560 Some("You must have at least one project open"),
561 &[PromptButton::ok("Ok")],
562 );
563 return Ok(());
564 };
565
566 let editor = this
567 .update_in(cx, |this, window, cx| {
568 this.workspace.update(cx, |workspace, cx| {
569 workspace.open_path(
570 ProjectPath {
571 worktree_id,
572 path: local_debug_file_relative_path().into(),
573 },
574 None,
575 true,
576 window,
577 cx,
578 )
579 })
580 })??
581 .await?;
582
583 cx.update(|_window, cx| {
584 if let Some(editor) = editor.act_as::<Editor>(cx) {
585 editor.update(cx, |editor, cx| {
586 editor.buffer().update(cx, |buffer, cx| {
587 if let Some(singleton) = buffer.as_singleton() {
588 singleton.update(cx, |buffer, cx| {
589 if buffer.is_empty() {
590 buffer.edit(
591 [(0..0, initial_local_debug_tasks_content())],
592 None,
593 cx,
594 );
595 }
596 })
597 }
598 })
599 });
600 }
601 })
602 .ok();
603
604 this.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
605
606 anyhow::Ok(())
607 })
608 .detach();
609 }
610}
611
612static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger");
613
614#[derive(Clone, Copy)]
615pub(crate) enum NewProcessMode {
616 Task,
617 Launch,
618 Attach,
619 Debug,
620}
621
622impl std::fmt::Display for NewProcessMode {
623 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
624 let mode = match self {
625 NewProcessMode::Task => "Run",
626 NewProcessMode::Debug => "Debug",
627 NewProcessMode::Attach => "Attach",
628 NewProcessMode::Launch => "Launch",
629 };
630
631 write!(f, "{}", mode)
632 }
633}
634
635impl Focusable for NewProcessMode {
636 fn focus_handle(&self, cx: &App) -> FocusHandle {
637 cx.focus_handle()
638 }
639}
640
641fn render_editor(editor: &Entity<Editor>, window: &mut Window, cx: &App) -> impl IntoElement {
642 let settings = ThemeSettings::get_global(cx);
643 let theme = cx.theme();
644
645 let text_style = TextStyle {
646 color: cx.theme().colors().text,
647 font_family: settings.buffer_font.family.clone(),
648 font_features: settings.buffer_font.features.clone(),
649 font_size: settings.buffer_font_size(cx).into(),
650 font_weight: settings.buffer_font.weight,
651 line_height: relative(settings.buffer_line_height.value()),
652 background_color: Some(theme.colors().editor_background),
653 ..Default::default()
654 };
655
656 let element = EditorElement::new(
657 editor,
658 EditorStyle {
659 background: theme.colors().editor_background,
660 local_player: theme.players().local(),
661 text: text_style,
662 ..Default::default()
663 },
664 );
665
666 div()
667 .rounded_md()
668 .p_1()
669 .border_1()
670 .border_color(theme.colors().border_variant)
671 .when(
672 editor.focus_handle(cx).contains_focused(window, cx),
673 |this| this.border_color(theme.colors().border_focused),
674 )
675 .child(element)
676 .bg(theme.colors().editor_background)
677}
678
679impl Render for NewProcessModal {
680 fn render(
681 &mut self,
682 window: &mut ui::Window,
683 cx: &mut ui::Context<Self>,
684 ) -> impl ui::IntoElement {
685 v_flex()
686 .size_full()
687 .w(rems(34.))
688 .key_context({
689 let mut key_context = KeyContext::new_with_defaults();
690 key_context.add("Pane");
691 key_context.add("RunModal");
692 key_context
693 })
694 .elevation_3(cx)
695 .bg(cx.theme().colors().elevated_surface_background)
696 .on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
697 cx.emit(DismissEvent);
698 }))
699 .on_action(cx.listener(|this, _: &pane::ActivateNextItem, window, cx| {
700 this.mode = match this.mode {
701 NewProcessMode::Task => NewProcessMode::Debug,
702 NewProcessMode::Debug => NewProcessMode::Attach,
703 NewProcessMode::Attach => NewProcessMode::Launch,
704 NewProcessMode::Launch => NewProcessMode::Task,
705 };
706
707 this.mode_focus_handle(cx).focus(window);
708 }))
709 .on_action(
710 cx.listener(|this, _: &pane::ActivatePreviousItem, window, cx| {
711 this.mode = match this.mode {
712 NewProcessMode::Task => NewProcessMode::Launch,
713 NewProcessMode::Debug => NewProcessMode::Task,
714 NewProcessMode::Attach => NewProcessMode::Debug,
715 NewProcessMode::Launch => NewProcessMode::Attach,
716 };
717
718 this.mode_focus_handle(cx).focus(window);
719 }),
720 )
721 .child(
722 h_flex()
723 .w_full()
724 .justify_around()
725 .p_2()
726 .child(
727 h_flex()
728 .justify_start()
729 .w_full()
730 .child(
731 ToggleButton::new(
732 "debugger-session-ui-tasks-button",
733 NewProcessMode::Task.to_string(),
734 )
735 .size(ButtonSize::Default)
736 .toggle_state(matches!(self.mode, NewProcessMode::Task))
737 .style(ui::ButtonStyle::Subtle)
738 .on_click(cx.listener(|this, _, window, cx| {
739 this.mode = NewProcessMode::Task;
740 this.mode_focus_handle(cx).focus(window);
741 cx.notify();
742 }))
743 .tooltip(Tooltip::text("Run predefined task"))
744 .first(),
745 )
746 .child(
747 ToggleButton::new(
748 "debugger-session-ui-launch-button",
749 NewProcessMode::Debug.to_string(),
750 )
751 .size(ButtonSize::Default)
752 .style(ui::ButtonStyle::Subtle)
753 .toggle_state(matches!(self.mode, NewProcessMode::Debug))
754 .on_click(cx.listener(|this, _, window, cx| {
755 this.mode = NewProcessMode::Debug;
756 this.mode_focus_handle(cx).focus(window);
757 cx.notify();
758 }))
759 .tooltip(Tooltip::text("Start a predefined debug scenario"))
760 .middle(),
761 )
762 .child(
763 ToggleButton::new(
764 "debugger-session-ui-attach-button",
765 NewProcessMode::Attach.to_string(),
766 )
767 .size(ButtonSize::Default)
768 .toggle_state(matches!(self.mode, NewProcessMode::Attach))
769 .style(ui::ButtonStyle::Subtle)
770 .on_click(cx.listener(|this, _, window, cx| {
771 this.mode = NewProcessMode::Attach;
772
773 if let Some(debugger) = this.debugger.as_ref() {
774 Self::update_attach_picker(
775 &this.attach_mode,
776 &debugger,
777 window,
778 cx,
779 );
780 }
781 this.mode_focus_handle(cx).focus(window);
782 cx.notify();
783 }))
784 .tooltip(Tooltip::text("Attach the debugger to a running process"))
785 .middle(),
786 )
787 .child(
788 ToggleButton::new(
789 "debugger-session-ui-custom-button",
790 NewProcessMode::Launch.to_string(),
791 )
792 .size(ButtonSize::Default)
793 .toggle_state(matches!(self.mode, NewProcessMode::Launch))
794 .style(ui::ButtonStyle::Subtle)
795 .on_click(cx.listener(|this, _, window, cx| {
796 this.mode = NewProcessMode::Launch;
797 this.mode_focus_handle(cx).focus(window);
798 cx.notify();
799 }))
800 .tooltip(Tooltip::text("Launch a new process with a debugger"))
801 .last(),
802 ),
803 )
804 .justify_between()
805 .border_color(cx.theme().colors().border_variant)
806 .border_b_1(),
807 )
808 .child(v_flex().child(self.render_mode(window, cx)))
809 .map(|el| {
810 let container = h_flex()
811 .justify_between()
812 .gap_2()
813 .p_2()
814 .border_color(cx.theme().colors().border_variant)
815 .border_t_1()
816 .w_full();
817 match self.mode {
818 NewProcessMode::Launch => el.child(
819 container
820 .child(
821 h_flex()
822 .text_ui_sm(cx)
823 .text_color(Color::Muted.color(cx))
824 .child(
825 InteractiveText::new(
826 "open-debug-json",
827 StyledText::new(
828 "Open .zed/debug.json for advanced configuration",
829 )
830 .with_highlights([(
831 5..20,
832 HighlightStyle {
833 underline: Some(UnderlineStyle {
834 thickness: px(1.0),
835 color: None,
836 wavy: false,
837 }),
838 ..Default::default()
839 },
840 )]),
841 )
842 .on_click(
843 vec![5..20],
844 {
845 let this = cx.entity();
846 move |_, window, cx| {
847 this.update(cx, |this, cx| {
848 this.open_debug_json(window, cx);
849 })
850 }
851 },
852 ),
853 ),
854 )
855 .child(
856 Button::new("debugger-spawn", "Start")
857 .on_click(cx.listener(|this, _, window, cx| {
858 this.start_new_session(window, cx)
859 }))
860 .disabled(
861 self.debugger.is_none()
862 || self
863 .configure_mode
864 .read(cx)
865 .program
866 .read(cx)
867 .is_empty(cx),
868 ),
869 ),
870 ),
871 NewProcessMode::Attach => el.child(
872 container
873 .child(div().child(self.adapter_drop_down_menu(window, cx)))
874 .child(
875 Button::new("debugger-spawn", "Start")
876 .on_click(cx.listener(|this, _, window, cx| {
877 this.start_new_session(window, cx)
878 }))
879 .disabled(
880 self.debugger.is_none()
881 || self
882 .attach_mode
883 .read(cx)
884 .attach_picker
885 .read(cx)
886 .picker
887 .read(cx)
888 .delegate
889 .match_count()
890 == 0,
891 ),
892 ),
893 ),
894 NewProcessMode::Debug => el,
895 NewProcessMode::Task => el,
896 }
897 })
898 }
899}
900
901impl EventEmitter<DismissEvent> for NewProcessModal {}
902impl Focusable for NewProcessModal {
903 fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle {
904 self.mode_focus_handle(cx)
905 }
906}
907
908impl ModalView for NewProcessModal {}
909
910impl RenderOnce for AttachMode {
911 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
912 v_flex()
913 .w_full()
914 .track_focus(&self.attach_picker.focus_handle(cx))
915 .child(self.attach_picker.clone())
916 }
917}
918
919#[derive(Clone)]
920pub(super) struct ConfigureMode {
921 program: Entity<Editor>,
922 cwd: Entity<Editor>,
923 stop_on_entry: ToggleState,
924 save_to_debug_json: ToggleState,
925}
926
927impl ConfigureMode {
928 pub(super) fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
929 let program = cx.new(|cx| Editor::single_line(window, cx));
930 program.update(cx, |this, cx| {
931 this.set_placeholder_text("ENV=Zed ~/bin/program --option", cx);
932 });
933
934 let cwd = cx.new(|cx| Editor::single_line(window, cx));
935 cwd.update(cx, |this, cx| {
936 this.set_placeholder_text("Ex: $ZED_WORKTREE_ROOT", cx);
937 });
938
939 cx.new(|_| Self {
940 program,
941 cwd,
942 stop_on_entry: ToggleState::Unselected,
943 save_to_debug_json: ToggleState::Unselected,
944 })
945 }
946
947 fn load(&mut self, cwd: PathBuf, window: &mut Window, cx: &mut App) {
948 self.cwd.update(cx, |editor, cx| {
949 if editor.is_empty(cx) {
950 editor.set_text(cwd.to_string_lossy(), window, cx);
951 }
952 });
953 }
954
955 pub(super) fn debug_request(&self, cx: &App) -> task::LaunchRequest {
956 let cwd_text = self.cwd.read(cx).text(cx);
957 let cwd = if cwd_text.is_empty() {
958 None
959 } else {
960 Some(PathBuf::from(cwd_text))
961 };
962
963 if cfg!(windows) {
964 return task::LaunchRequest {
965 program: self.program.read(cx).text(cx),
966 cwd,
967 args: Default::default(),
968 env: Default::default(),
969 };
970 }
971 let command = self.program.read(cx).text(cx);
972 let mut args = shlex::split(&command).into_iter().flatten().peekable();
973 let mut env = FxHashMap::default();
974 while args.peek().is_some_and(|arg| arg.contains('=')) {
975 let arg = args.next().unwrap();
976 let (lhs, rhs) = arg.split_once('=').unwrap();
977 env.insert(lhs.to_string(), rhs.to_string());
978 }
979
980 let program = if let Some(program) = args.next() {
981 program
982 } else {
983 env = FxHashMap::default();
984 command
985 };
986
987 let args = args.collect::<Vec<_>>();
988
989 task::LaunchRequest {
990 program,
991 cwd,
992 args,
993 env,
994 }
995 }
996
997 fn render(
998 &mut self,
999 adapter_menu: DropdownMenu,
1000 window: &mut Window,
1001 cx: &mut ui::Context<Self>,
1002 ) -> impl IntoElement {
1003 v_flex()
1004 .p_2()
1005 .w_full()
1006 .gap_3()
1007 .track_focus(&self.program.focus_handle(cx))
1008 .child(
1009 h_flex()
1010 .child(
1011 Label::new("Debugger")
1012 .size(ui::LabelSize::Small)
1013 .color(Color::Muted),
1014 )
1015 .gap(ui::DynamicSpacing::Base08.rems(cx))
1016 .child(adapter_menu),
1017 )
1018 .child(
1019 Label::new("Program")
1020 .size(ui::LabelSize::Small)
1021 .color(Color::Muted),
1022 )
1023 .child(render_editor(&self.program, window, cx))
1024 .child(
1025 Label::new("Working Directory")
1026 .size(ui::LabelSize::Small)
1027 .color(Color::Muted),
1028 )
1029 .child(render_editor(&self.cwd, window, cx))
1030 .child(
1031 CheckboxWithLabel::new(
1032 "debugger-stop-on-entry",
1033 Label::new("Stop on Entry")
1034 .size(ui::LabelSize::Small)
1035 .color(Color::Muted),
1036 self.stop_on_entry,
1037 {
1038 let this = cx.weak_entity();
1039 move |state, _, cx| {
1040 this.update(cx, |this, _| {
1041 this.stop_on_entry = *state;
1042 })
1043 .ok();
1044 }
1045 },
1046 )
1047 .checkbox_position(ui::IconPosition::End),
1048 )
1049 .child(
1050 CheckboxWithLabel::new(
1051 "debugger-save-to-debug-json",
1052 Label::new("Save to debug.json")
1053 .size(ui::LabelSize::Small)
1054 .color(Color::Muted),
1055 self.save_to_debug_json,
1056 {
1057 let this = cx.weak_entity();
1058 move |state, _, cx| {
1059 this.update(cx, |this, _| {
1060 this.save_to_debug_json = *state;
1061 })
1062 .ok();
1063 }
1064 },
1065 )
1066 .checkbox_position(ui::IconPosition::End),
1067 )
1068 }
1069}
1070
1071#[derive(Clone)]
1072pub(super) struct AttachMode {
1073 pub(super) definition: ZedDebugConfig,
1074 pub(super) attach_picker: Entity<AttachModal>,
1075}
1076
1077impl AttachMode {
1078 pub(super) fn new(
1079 debugger: Option<DebugAdapterName>,
1080 workspace: WeakEntity<Workspace>,
1081 window: &mut Window,
1082 cx: &mut Context<NewProcessModal>,
1083 ) -> Entity<Self> {
1084 let definition = ZedDebugConfig {
1085 adapter: debugger.unwrap_or(DebugAdapterName("".into())).0,
1086 label: "Attach New Session Setup".into(),
1087 request: dap::DebugRequest::Attach(task::AttachRequest { process_id: None }),
1088 stop_on_entry: Some(false),
1089 };
1090 let attach_picker = cx.new(|cx| {
1091 let modal = AttachModal::new(definition.clone(), workspace, false, window, cx);
1092 window.focus(&modal.focus_handle(cx));
1093
1094 modal
1095 });
1096
1097 cx.new(|_| Self {
1098 definition,
1099 attach_picker,
1100 })
1101 }
1102 pub(super) fn debug_request(&self) -> task::AttachRequest {
1103 task::AttachRequest { process_id: None }
1104 }
1105}
1106
1107#[derive(Clone)]
1108pub(super) struct TaskMode {
1109 pub(super) task_modal: Entity<TasksModal>,
1110}
1111
1112pub(super) struct DebugDelegate {
1113 task_store: Entity<TaskStore>,
1114 candidates: Vec<(Option<TaskSourceKind>, DebugScenario)>,
1115 selected_index: usize,
1116 matches: Vec<StringMatch>,
1117 prompt: String,
1118 debug_panel: WeakEntity<DebugPanel>,
1119 task_contexts: Option<Arc<TaskContexts>>,
1120 divider_index: Option<usize>,
1121 last_used_candidate_index: Option<usize>,
1122}
1123
1124impl DebugDelegate {
1125 pub(super) fn new(debug_panel: WeakEntity<DebugPanel>, task_store: Entity<TaskStore>) -> Self {
1126 Self {
1127 task_store,
1128 candidates: Vec::default(),
1129 selected_index: 0,
1130 matches: Vec::new(),
1131 prompt: String::new(),
1132 debug_panel,
1133 task_contexts: None,
1134 divider_index: None,
1135 last_used_candidate_index: None,
1136 }
1137 }
1138
1139 fn get_scenario_kind(
1140 languages: &Arc<LanguageRegistry>,
1141 dap_registry: &DapRegistry,
1142 scenario: DebugScenario,
1143 ) -> (Option<TaskSourceKind>, DebugScenario) {
1144 let language_names = languages.language_names();
1145 let language = dap_registry
1146 .adapter_language(&scenario.adapter)
1147 .map(|language| TaskSourceKind::Language {
1148 name: language.into(),
1149 });
1150
1151 let language = language.or_else(|| {
1152 scenario.label.split_whitespace().find_map(|word| {
1153 language_names
1154 .iter()
1155 .find(|name| name.eq_ignore_ascii_case(word))
1156 .map(|name| TaskSourceKind::Language {
1157 name: name.to_owned().into(),
1158 })
1159 })
1160 });
1161
1162 (language, scenario)
1163 }
1164
1165 pub fn tasks_loaded(
1166 &mut self,
1167 task_contexts: Arc<TaskContexts>,
1168 languages: Arc<LanguageRegistry>,
1169 lsp_tasks: Vec<(TaskSourceKind, task::ResolvedTask)>,
1170 current_resolved_tasks: Vec<(TaskSourceKind, task::ResolvedTask)>,
1171 add_current_language_tasks: bool,
1172 cx: &mut Context<Picker<Self>>,
1173 ) -> Task<()> {
1174 self.task_contexts = Some(task_contexts.clone());
1175 let task = self.task_store.update(cx, |task_store, cx| {
1176 task_store.task_inventory().map(|inventory| {
1177 inventory.update(cx, |inventory, cx| {
1178 inventory.list_debug_scenarios(
1179 &task_contexts,
1180 lsp_tasks,
1181 current_resolved_tasks,
1182 add_current_language_tasks,
1183 cx,
1184 )
1185 })
1186 })
1187 });
1188 cx.spawn(async move |this, cx| {
1189 let (recent, scenarios) = if let Some(task) = task {
1190 task.await
1191 } else {
1192 (Vec::new(), Vec::new())
1193 };
1194
1195 this.update(cx, |this, cx| {
1196 if !recent.is_empty() {
1197 this.delegate.last_used_candidate_index = Some(recent.len() - 1);
1198 }
1199
1200 let dap_registry = cx.global::<DapRegistry>();
1201 let hide_vscode = scenarios.iter().any(|(kind, _)| match kind {
1202 TaskSourceKind::Worktree {
1203 id: _,
1204 directory_in_worktree: dir,
1205 id_base: _,
1206 } => dir.ends_with(".zed"),
1207 _ => false,
1208 });
1209
1210 this.delegate.candidates = recent
1211 .into_iter()
1212 .map(|scenario| Self::get_scenario_kind(&languages, &dap_registry, scenario))
1213 .chain(
1214 scenarios
1215 .into_iter()
1216 .filter(|(kind, _)| match kind {
1217 TaskSourceKind::Worktree {
1218 id: _,
1219 directory_in_worktree: dir,
1220 id_base: _,
1221 } => !(hide_vscode && dir.ends_with(".vscode")),
1222 _ => true,
1223 })
1224 .map(|(kind, scenario)| {
1225 let (language, scenario) =
1226 Self::get_scenario_kind(&languages, &dap_registry, scenario);
1227 (language.or(Some(kind)), scenario)
1228 }),
1229 )
1230 .collect();
1231 })
1232 .ok();
1233 })
1234 }
1235}
1236
1237impl PickerDelegate for DebugDelegate {
1238 type ListItem = ui::ListItem;
1239
1240 fn match_count(&self) -> usize {
1241 self.matches.len()
1242 }
1243
1244 fn selected_index(&self) -> usize {
1245 self.selected_index
1246 }
1247
1248 fn set_selected_index(
1249 &mut self,
1250 ix: usize,
1251 _window: &mut Window,
1252 _cx: &mut Context<picker::Picker<Self>>,
1253 ) {
1254 self.selected_index = ix;
1255 }
1256
1257 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> {
1258 "Find a debug task, or debug a command.".into()
1259 }
1260
1261 fn update_matches(
1262 &mut self,
1263 query: String,
1264 window: &mut Window,
1265 cx: &mut Context<picker::Picker<Self>>,
1266 ) -> gpui::Task<()> {
1267 let candidates = self.candidates.clone();
1268
1269 cx.spawn_in(window, async move |picker, cx| {
1270 let candidates: Vec<_> = candidates
1271 .into_iter()
1272 .enumerate()
1273 .map(|(index, (_, candidate))| {
1274 StringMatchCandidate::new(index, candidate.label.as_ref())
1275 })
1276 .collect();
1277
1278 let matches = fuzzy::match_strings(
1279 &candidates,
1280 &query,
1281 true,
1282 true,
1283 1000,
1284 &Default::default(),
1285 cx.background_executor().clone(),
1286 )
1287 .await;
1288
1289 picker
1290 .update(cx, |picker, _| {
1291 let delegate = &mut picker.delegate;
1292
1293 delegate.matches = matches;
1294 delegate.prompt = query;
1295
1296 delegate.divider_index = delegate.last_used_candidate_index.and_then(|index| {
1297 let index = delegate
1298 .matches
1299 .partition_point(|matching_task| matching_task.candidate_id <= index);
1300 Some(index).and_then(|index| (index != 0).then(|| index - 1))
1301 });
1302
1303 if delegate.matches.is_empty() {
1304 delegate.selected_index = 0;
1305 } else {
1306 delegate.selected_index =
1307 delegate.selected_index.min(delegate.matches.len() - 1);
1308 }
1309 })
1310 .log_err();
1311 })
1312 }
1313
1314 fn separators_after_indices(&self) -> Vec<usize> {
1315 if let Some(i) = self.divider_index {
1316 vec![i]
1317 } else {
1318 Vec::new()
1319 }
1320 }
1321
1322 fn confirm_input(
1323 &mut self,
1324 _secondary: bool,
1325 window: &mut Window,
1326 cx: &mut Context<Picker<Self>>,
1327 ) {
1328 let text = self.prompt.clone();
1329 let (task_context, worktree_id) = self
1330 .task_contexts
1331 .as_ref()
1332 .and_then(|task_contexts| {
1333 Some((
1334 task_contexts.active_context().cloned()?,
1335 task_contexts.worktree(),
1336 ))
1337 })
1338 .unwrap_or_default();
1339
1340 let mut args = shlex::split(&text).into_iter().flatten().peekable();
1341 let mut env = HashMap::default();
1342 while args.peek().is_some_and(|arg| arg.contains('=')) {
1343 let arg = args.next().unwrap();
1344 let (lhs, rhs) = arg.split_once('=').unwrap();
1345 env.insert(lhs.to_string(), rhs.to_string());
1346 }
1347
1348 let program = if let Some(program) = args.next() {
1349 program
1350 } else {
1351 env = HashMap::default();
1352 text
1353 };
1354
1355 let args = args.collect::<Vec<_>>();
1356 let task = task::TaskTemplate {
1357 label: "one-off".to_owned(),
1358 env,
1359 command: program,
1360 args,
1361 ..Default::default()
1362 };
1363
1364 let Some(location) = self
1365 .task_contexts
1366 .as_ref()
1367 .and_then(|cx| cx.location().cloned())
1368 else {
1369 return;
1370 };
1371 let file = location.buffer.read(cx).file();
1372 let language = location.buffer.read(cx).language();
1373 let language_name = language.as_ref().map(|l| l.name());
1374 let Some(adapter): Option<DebugAdapterName> =
1375 language::language_settings::language_settings(language_name, file, cx)
1376 .debuggers
1377 .first()
1378 .map(SharedString::from)
1379 .map(Into::into)
1380 .or_else(|| {
1381 language.and_then(|l| {
1382 l.config()
1383 .debuggers
1384 .first()
1385 .map(SharedString::from)
1386 .map(Into::into)
1387 })
1388 })
1389 else {
1390 return;
1391 };
1392 let locators = cx.global::<DapRegistry>().locators();
1393 cx.spawn_in(window, async move |this, cx| {
1394 let Some(debug_scenario) = cx
1395 .background_spawn(async move {
1396 for locator in locators {
1397 if let Some(scenario) =
1398 locator.1.create_scenario(&task, "one-off", &adapter).await
1399 {
1400 return Some(scenario);
1401 }
1402 }
1403 None
1404 })
1405 .await
1406 else {
1407 return;
1408 };
1409
1410 this.update_in(cx, |this, window, cx| {
1411 send_telemetry(&debug_scenario, TelemetrySpawnLocation::ScenarioList, cx);
1412 this.delegate
1413 .debug_panel
1414 .update(cx, |panel, cx| {
1415 panel.start_session(
1416 debug_scenario,
1417 task_context,
1418 None,
1419 worktree_id,
1420 window,
1421 cx,
1422 );
1423 })
1424 .ok();
1425 cx.emit(DismissEvent);
1426 })
1427 .ok();
1428 })
1429 .detach();
1430 }
1431
1432 fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
1433 let debug_scenario = self
1434 .matches
1435 .get(self.selected_index())
1436 .and_then(|match_candidate| self.candidates.get(match_candidate.candidate_id).cloned());
1437
1438 let Some((_, debug_scenario)) = debug_scenario else {
1439 return;
1440 };
1441
1442 let (task_context, worktree_id) = self
1443 .task_contexts
1444 .as_ref()
1445 .and_then(|task_contexts| {
1446 Some((
1447 task_contexts.active_context().cloned()?,
1448 task_contexts.worktree(),
1449 ))
1450 })
1451 .unwrap_or_default();
1452
1453 send_telemetry(&debug_scenario, TelemetrySpawnLocation::ScenarioList, cx);
1454 self.debug_panel
1455 .update(cx, |panel, cx| {
1456 panel.start_session(debug_scenario, task_context, None, worktree_id, window, cx);
1457 })
1458 .ok();
1459
1460 cx.emit(DismissEvent);
1461 }
1462
1463 fn dismissed(&mut self, _: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
1464 cx.emit(DismissEvent);
1465 }
1466
1467 fn render_footer(
1468 &self,
1469 window: &mut Window,
1470 cx: &mut Context<Picker<Self>>,
1471 ) -> Option<ui::AnyElement> {
1472 let current_modifiers = window.modifiers();
1473 let footer = h_flex()
1474 .w_full()
1475 .h_8()
1476 .p_2()
1477 .justify_between()
1478 .rounded_b_sm()
1479 .bg(cx.theme().colors().ghost_element_selected)
1480 .border_t_1()
1481 .border_color(cx.theme().colors().border_variant)
1482 .child(
1483 // TODO: add button to open selected task in debug.json
1484 h_flex().into_any_element(),
1485 )
1486 .map(|this| {
1487 if (current_modifiers.alt || self.matches.is_empty()) && !self.prompt.is_empty() {
1488 let action = picker::ConfirmInput {
1489 secondary: current_modifiers.secondary(),
1490 }
1491 .boxed_clone();
1492 this.children(KeyBinding::for_action(&*action, window, cx).map(|keybind| {
1493 Button::new("launch-custom", "Launch Custom")
1494 .label_size(LabelSize::Small)
1495 .key_binding(keybind)
1496 .on_click(move |_, window, cx| {
1497 window.dispatch_action(action.boxed_clone(), cx)
1498 })
1499 }))
1500 } else {
1501 this.children(KeyBinding::for_action(&menu::Confirm, window, cx).map(
1502 |keybind| {
1503 let is_recent_selected =
1504 self.divider_index >= Some(self.selected_index);
1505 let run_entry_label =
1506 if is_recent_selected { "Rerun" } else { "Spawn" };
1507
1508 Button::new("spawn", run_entry_label)
1509 .label_size(LabelSize::Small)
1510 .key_binding(keybind)
1511 .on_click(|_, window, cx| {
1512 window.dispatch_action(menu::Confirm.boxed_clone(), cx);
1513 })
1514 },
1515 ))
1516 }
1517 });
1518 Some(footer.into_any_element())
1519 }
1520
1521 fn render_match(
1522 &self,
1523 ix: usize,
1524 selected: bool,
1525 window: &mut Window,
1526 cx: &mut Context<picker::Picker<Self>>,
1527 ) -> Option<Self::ListItem> {
1528 let hit = &self.matches[ix];
1529
1530 let highlighted_location = HighlightedMatch {
1531 text: hit.string.clone(),
1532 highlight_positions: hit.positions.clone(),
1533 char_count: hit.string.chars().count(),
1534 color: Color::Default,
1535 };
1536 let task_kind = &self.candidates[hit.candidate_id].0;
1537
1538 let icon = match task_kind {
1539 Some(TaskSourceKind::UserInput) => Some(Icon::new(IconName::Terminal)),
1540 Some(TaskSourceKind::AbsPath { .. }) => Some(Icon::new(IconName::Settings)),
1541 Some(TaskSourceKind::Worktree { .. }) => Some(Icon::new(IconName::FileTree)),
1542 Some(TaskSourceKind::Lsp {
1543 language_name: name,
1544 ..
1545 })
1546 | Some(TaskSourceKind::Language { name }) => file_icons::FileIcons::get(cx)
1547 .get_icon_for_type(&name.to_lowercase(), cx)
1548 .map(Icon::from_path),
1549 None => Some(Icon::new(IconName::HistoryRerun)),
1550 }
1551 .map(|icon| icon.color(Color::Muted).size(IconSize::Small));
1552 let indicator = if matches!(task_kind, Some(TaskSourceKind::Lsp { .. })) {
1553 Some(Indicator::icon(
1554 Icon::new(IconName::BoltFilled)
1555 .color(Color::Muted)
1556 .size(IconSize::Small),
1557 ))
1558 } else {
1559 None
1560 };
1561 let icon = icon.map(|icon| {
1562 IconWithIndicator::new(icon, indicator)
1563 .indicator_border_color(Some(cx.theme().colors().border_transparent))
1564 });
1565
1566 Some(
1567 ListItem::new(SharedString::from(format!("debug-scenario-selection-{ix}")))
1568 .inset(true)
1569 .start_slot::<IconWithIndicator>(icon)
1570 .spacing(ListItemSpacing::Sparse)
1571 .toggle_state(selected)
1572 .child(highlighted_location.render(window, cx)),
1573 )
1574 }
1575}
1576
1577pub(crate) fn resolve_path(path: &mut String) {
1578 if path.starts_with('~') {
1579 let home = paths::home_dir().to_string_lossy().to_string();
1580 let trimmed_path = path.trim().to_owned();
1581 *path = trimmed_path.replacen('~', &home, 1);
1582 } else if let Some(strip_path) = path.strip_prefix(&format!(".{}", std::path::MAIN_SEPARATOR)) {
1583 *path = format!(
1584 "$ZED_WORKTREE_ROOT{}{}",
1585 std::path::MAIN_SEPARATOR,
1586 &strip_path
1587 );
1588 };
1589}