1use std::path::Path;
2use std::sync::Arc;
3
4use anyhow::{Result, anyhow};
5use dap::StackFrameId;
6use gpui::{
7 AnyElement, Entity, EventEmitter, FocusHandle, Focusable, ListState, Subscription, Task,
8 WeakEntity, list,
9};
10
11use language::PointUtf16;
12use project::debugger::session::{Session, SessionEvent, StackFrame};
13use project::{ProjectItem, ProjectPath};
14use ui::{Tooltip, prelude::*};
15use util::ResultExt;
16use workspace::Workspace;
17
18use super::RunningState;
19
20#[derive(Debug)]
21pub enum StackFrameListEvent {
22 SelectedStackFrameChanged(StackFrameId),
23}
24
25pub struct StackFrameList {
26 list: ListState,
27 focus_handle: FocusHandle,
28 _subscription: Subscription,
29 session: Entity<Session>,
30 state: WeakEntity<RunningState>,
31 invalidate: bool,
32 entries: Vec<StackFrameEntry>,
33 workspace: WeakEntity<Workspace>,
34 current_stack_frame_id: Option<StackFrameId>,
35}
36
37#[allow(clippy::large_enum_variant)]
38#[derive(Debug, PartialEq, Eq)]
39pub enum StackFrameEntry {
40 Normal(dap::StackFrame),
41 Collapsed(Vec<dap::StackFrame>),
42}
43
44impl StackFrameList {
45 pub fn new(
46 workspace: WeakEntity<Workspace>,
47 session: Entity<Session>,
48 state: WeakEntity<RunningState>,
49 window: &mut Window,
50 cx: &mut Context<Self>,
51 ) -> Self {
52 let weak_entity = cx.weak_entity();
53 let focus_handle = cx.focus_handle();
54
55 let list = ListState::new(
56 0,
57 gpui::ListAlignment::Top,
58 px(1000.),
59 move |ix, _window, cx| {
60 weak_entity
61 .upgrade()
62 .map(|stack_frame_list| {
63 stack_frame_list.update(cx, |this, cx| this.render_entry(ix, cx))
64 })
65 .unwrap_or(div().into_any())
66 },
67 );
68
69 let _subscription =
70 cx.subscribe_in(&session, window, |this, _, event, _, cx| match event {
71 SessionEvent::Stopped(_) | SessionEvent::StackTrace | SessionEvent::Threads => {
72 this.refresh(cx);
73 }
74 _ => {}
75 });
76
77 Self {
78 list,
79 session,
80 workspace,
81 focus_handle,
82 state,
83 _subscription,
84 invalidate: true,
85 entries: Default::default(),
86 current_stack_frame_id: None,
87 }
88 }
89
90 #[cfg(test)]
91 pub(crate) fn entries(&self) -> &Vec<StackFrameEntry> {
92 &self.entries
93 }
94
95 #[cfg(test)]
96 pub(crate) fn flatten_entries(&self) -> Vec<dap::StackFrame> {
97 self.entries
98 .iter()
99 .flat_map(|frame| match frame {
100 StackFrameEntry::Normal(frame) => vec![frame.clone()],
101 StackFrameEntry::Collapsed(frames) => frames.clone(),
102 })
103 .collect::<Vec<_>>()
104 }
105
106 fn stack_frames(&self, cx: &mut App) -> Vec<StackFrame> {
107 self.state
108 .read_with(cx, |state, _| state.thread_id)
109 .log_err()
110 .flatten()
111 .map(|thread_id| {
112 self.session
113 .update(cx, |this, cx| this.stack_frames(thread_id, cx))
114 })
115 .unwrap_or_default()
116 }
117
118 #[cfg(test)]
119 pub(crate) fn dap_stack_frames(&self, cx: &mut App) -> Vec<dap::StackFrame> {
120 self.stack_frames(cx)
121 .into_iter()
122 .map(|stack_frame| stack_frame.dap.clone())
123 .collect()
124 }
125
126 pub fn _get_main_stack_frame_id(&self, cx: &mut Context<Self>) -> u64 {
127 self.stack_frames(cx)
128 .first()
129 .map(|stack_frame| stack_frame.dap.id)
130 .unwrap_or(0)
131 }
132
133 pub fn current_stack_frame_id(&self) -> Option<StackFrameId> {
134 self.current_stack_frame_id
135 }
136
137 pub(super) fn refresh(&mut self, cx: &mut Context<Self>) {
138 self.invalidate = true;
139 self.entries.clear();
140 cx.notify();
141 }
142
143 pub fn build_entries(
144 &mut self,
145 select_first_stack_frame: bool,
146 window: &mut Window,
147 cx: &mut Context<Self>,
148 ) {
149 let mut entries = Vec::new();
150 let mut collapsed_entries = Vec::new();
151 let mut current_stack_frame = None;
152
153 let stack_frames = self.stack_frames(cx);
154 for stack_frame in &stack_frames {
155 match stack_frame.dap.presentation_hint {
156 Some(dap::StackFramePresentationHint::Deemphasize) => {
157 collapsed_entries.push(stack_frame.dap.clone());
158 }
159 _ => {
160 let collapsed_entries = std::mem::take(&mut collapsed_entries);
161 if !collapsed_entries.is_empty() {
162 entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone()));
163 }
164
165 current_stack_frame.get_or_insert(&stack_frame.dap);
166 entries.push(StackFrameEntry::Normal(stack_frame.dap.clone()));
167 }
168 }
169 }
170
171 let collapsed_entries = std::mem::take(&mut collapsed_entries);
172 if !collapsed_entries.is_empty() {
173 entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone()));
174 }
175
176 std::mem::swap(&mut self.entries, &mut entries);
177 self.list.reset(self.entries.len());
178
179 if let Some(current_stack_frame) = current_stack_frame.filter(|_| select_first_stack_frame)
180 {
181 self.select_stack_frame(current_stack_frame, true, window, cx)
182 .detach_and_log_err(cx);
183 }
184
185 cx.notify();
186 }
187
188 pub fn go_to_selected_stack_frame(&mut self, window: &Window, cx: &mut Context<Self>) {
189 if let Some(current_stack_frame_id) = self.current_stack_frame_id {
190 let frame = self
191 .entries
192 .iter()
193 .find_map(|entry| match entry {
194 StackFrameEntry::Normal(dap) => {
195 if dap.id == current_stack_frame_id {
196 Some(dap)
197 } else {
198 None
199 }
200 }
201 StackFrameEntry::Collapsed(daps) => {
202 daps.iter().find(|dap| dap.id == current_stack_frame_id)
203 }
204 })
205 .cloned();
206
207 if let Some(frame) = frame.as_ref() {
208 self.select_stack_frame(frame, true, window, cx)
209 .detach_and_log_err(cx);
210 }
211 }
212 }
213
214 pub fn select_stack_frame(
215 &mut self,
216 stack_frame: &dap::StackFrame,
217 go_to_stack_frame: bool,
218 window: &Window,
219 cx: &mut Context<Self>,
220 ) -> Task<Result<()>> {
221 self.current_stack_frame_id = Some(stack_frame.id);
222
223 cx.emit(StackFrameListEvent::SelectedStackFrameChanged(
224 stack_frame.id,
225 ));
226 cx.notify();
227
228 if !go_to_stack_frame {
229 return Task::ready(Ok(()));
230 };
231
232 let row = (stack_frame.line.saturating_sub(1)) as u32;
233
234 let Some(abs_path) = self.abs_path_from_stack_frame(&stack_frame) else {
235 return Task::ready(Err(anyhow!("Project path not found")));
236 };
237
238 cx.spawn_in(window, async move |this, cx| {
239 let (worktree, relative_path) = this
240 .update(cx, |this, cx| {
241 this.workspace.update(cx, |workspace, cx| {
242 workspace.project().update(cx, |this, cx| {
243 this.find_or_create_worktree(&abs_path, false, cx)
244 })
245 })
246 })??
247 .await?;
248 let buffer = this
249 .update(cx, |this, cx| {
250 this.workspace.update(cx, |this, cx| {
251 this.project().update(cx, |this, cx| {
252 let worktree_id = worktree.read(cx).id();
253 this.open_buffer(
254 ProjectPath {
255 worktree_id,
256 path: relative_path.into(),
257 },
258 cx,
259 )
260 })
261 })
262 })??
263 .await?;
264 let position = buffer.update(cx, |this, _| {
265 this.snapshot().anchor_after(PointUtf16::new(row, 0))
266 })?;
267 this.update_in(cx, |this, window, cx| {
268 this.workspace.update(cx, |workspace, cx| {
269 let project_path = buffer.read(cx).project_path(cx).ok_or_else(|| {
270 anyhow!("Could not select a stack frame for unnamed buffer")
271 })?;
272 anyhow::Ok(workspace.open_path_preview(
273 project_path,
274 None,
275 false,
276 true,
277 true,
278 window,
279 cx,
280 ))
281 })
282 })???
283 .await?;
284
285 this.update(cx, |this, cx| {
286 this.workspace.update(cx, |workspace, cx| {
287 let breakpoint_store = workspace.project().read(cx).breakpoint_store();
288
289 breakpoint_store.update(cx, |store, cx| {
290 store.set_active_position(
291 (this.session.read(cx).session_id(), abs_path, position),
292 cx,
293 );
294 })
295 })
296 })?
297 })
298 }
299
300 fn abs_path_from_stack_frame(&self, stack_frame: &dap::StackFrame) -> Option<Arc<Path>> {
301 stack_frame.source.as_ref().and_then(|s| {
302 s.path
303 .as_deref()
304 .map(|path| Arc::<Path>::from(Path::new(path)))
305 })
306 }
307
308 pub fn restart_stack_frame(&mut self, stack_frame_id: u64, cx: &mut Context<Self>) {
309 self.session.update(cx, |state, cx| {
310 state.restart_stack_frame(stack_frame_id, cx)
311 });
312 }
313
314 fn render_normal_entry(
315 &self,
316 stack_frame: &dap::StackFrame,
317 cx: &mut Context<Self>,
318 ) -> AnyElement {
319 let source = stack_frame.source.clone();
320 let is_selected_frame = Some(stack_frame.id) == self.current_stack_frame_id;
321
322 let formatted_path = format!(
323 "{}:{}",
324 source.clone().and_then(|s| s.name).unwrap_or_default(),
325 stack_frame.line,
326 );
327
328 let supports_frame_restart = self
329 .session
330 .read(cx)
331 .capabilities()
332 .supports_restart_frame
333 .unwrap_or_default();
334
335 let origin = stack_frame
336 .source
337 .to_owned()
338 .and_then(|source| source.origin);
339
340 h_flex()
341 .rounded_md()
342 .justify_between()
343 .w_full()
344 .group("")
345 .id(("stack-frame", stack_frame.id))
346 .tooltip({
347 let formatted_path = formatted_path.clone();
348 move |_window, app| {
349 app.new(|_| {
350 let mut tooltip = Tooltip::new(formatted_path.clone());
351
352 if let Some(origin) = &origin {
353 tooltip = tooltip.meta(origin);
354 }
355
356 tooltip
357 })
358 .into()
359 }
360 })
361 .p_1()
362 .when(is_selected_frame, |this| {
363 this.bg(cx.theme().colors().element_hover)
364 })
365 .on_click(cx.listener({
366 let stack_frame = stack_frame.clone();
367 move |this, _, window, cx| {
368 this.select_stack_frame(&stack_frame, true, window, cx)
369 .detach_and_log_err(cx);
370 }
371 }))
372 .hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
373 .child(
374 v_flex()
375 .child(
376 h_flex()
377 .gap_0p5()
378 .text_ui_sm(cx)
379 .truncate()
380 .child(stack_frame.name.clone())
381 .child(formatted_path),
382 )
383 .child(
384 h_flex()
385 .text_ui_xs(cx)
386 .truncate()
387 .text_color(cx.theme().colors().text_muted)
388 .when_some(source.and_then(|s| s.path), |this, path| this.child(path)),
389 ),
390 )
391 .when(
392 supports_frame_restart && stack_frame.can_restart.unwrap_or(true),
393 |this| {
394 this.child(
395 h_flex()
396 .id(("restart-stack-frame", stack_frame.id))
397 .visible_on_hover("")
398 .absolute()
399 .right_2()
400 .overflow_hidden()
401 .rounded_md()
402 .border_1()
403 .border_color(cx.theme().colors().element_selected)
404 .bg(cx.theme().colors().element_background)
405 .hover(|style| {
406 style
407 .bg(cx.theme().colors().ghost_element_hover)
408 .cursor_pointer()
409 })
410 .child(
411 IconButton::new(
412 ("restart-stack-frame", stack_frame.id),
413 IconName::DebugRestart,
414 )
415 .icon_size(IconSize::Small)
416 .on_click(cx.listener({
417 let stack_frame_id = stack_frame.id;
418 move |this, _, _window, cx| {
419 this.restart_stack_frame(stack_frame_id, cx);
420 }
421 }))
422 .tooltip(move |window, cx| {
423 Tooltip::text("Restart Stack Frame")(window, cx)
424 }),
425 ),
426 )
427 },
428 )
429 .into_any()
430 }
431
432 pub fn expand_collapsed_entry(
433 &mut self,
434 ix: usize,
435 stack_frames: &Vec<dap::StackFrame>,
436 cx: &mut Context<Self>,
437 ) {
438 self.entries.splice(
439 ix..ix + 1,
440 stack_frames
441 .iter()
442 .map(|frame| StackFrameEntry::Normal(frame.clone())),
443 );
444 self.list.reset(self.entries.len());
445 cx.notify();
446 }
447
448 fn render_collapsed_entry(
449 &self,
450 ix: usize,
451 stack_frames: &Vec<dap::StackFrame>,
452 cx: &mut Context<Self>,
453 ) -> AnyElement {
454 let first_stack_frame = &stack_frames[0];
455
456 h_flex()
457 .rounded_md()
458 .justify_between()
459 .w_full()
460 .group("")
461 .id(("stack-frame", first_stack_frame.id))
462 .p_1()
463 .on_click(cx.listener({
464 let stack_frames = stack_frames.clone();
465 move |this, _, _window, cx| {
466 this.expand_collapsed_entry(ix, &stack_frames, cx);
467 }
468 }))
469 .hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
470 .child(
471 v_flex()
472 .text_ui_sm(cx)
473 .truncate()
474 .text_color(cx.theme().colors().text_muted)
475 .child(format!(
476 "Show {} more{}",
477 stack_frames.len(),
478 first_stack_frame
479 .source
480 .as_ref()
481 .and_then(|source| source.origin.as_ref())
482 .map_or(String::new(), |origin| format!(": {}", origin))
483 )),
484 )
485 .into_any()
486 }
487
488 fn render_entry(&self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
489 match &self.entries[ix] {
490 StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(stack_frame, cx),
491 StackFrameEntry::Collapsed(stack_frames) => {
492 self.render_collapsed_entry(ix, stack_frames, cx)
493 }
494 }
495 }
496}
497
498impl Render for StackFrameList {
499 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
500 if self.invalidate {
501 self.build_entries(self.entries.is_empty(), window, cx);
502 self.invalidate = false;
503 cx.notify();
504 }
505
506 div()
507 .size_full()
508 .p_1()
509 .child(list(self.list.clone()).size_full())
510 }
511}
512
513impl Focusable for StackFrameList {
514 fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
515 self.focus_handle.clone()
516 }
517}
518
519impl EventEmitter<StackFrameListEvent> for StackFrameList {}