running.rs

  1mod console;
  2mod loaded_source_list;
  3mod module_list;
  4pub mod stack_frame_list;
  5pub mod variable_list;
  6
  7use super::{DebugPanelItemEvent, ThreadItem};
  8use console::Console;
  9use dap::{client::SessionId, debugger_settings::DebuggerSettings, Capabilities, Thread};
 10use gpui::{AppContext, Entity, EventEmitter, FocusHandle, Focusable, Subscription, WeakEntity};
 11use loaded_source_list::LoadedSourceList;
 12use module_list::ModuleList;
 13use project::debugger::session::{Session, SessionEvent, ThreadId, ThreadStatus};
 14use rpc::proto::ViewId;
 15use settings::Settings;
 16use stack_frame_list::StackFrameList;
 17use ui::{
 18    div, h_flex, v_flex, ActiveTheme, AnyElement, App, Button, ButtonCommon, Clickable, Context,
 19    ContextMenu, Disableable, DropdownMenu, FluentBuilder, IconButton, IconName, IconSize,
 20    Indicator, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
 21    StatefulInteractiveElement, Styled, Tooltip, Window,
 22};
 23use util::ResultExt;
 24use variable_list::VariableList;
 25use workspace::Workspace;
 26
 27pub struct RunningState {
 28    session: Entity<Session>,
 29    thread_id: Option<ThreadId>,
 30    console: Entity<console::Console>,
 31    focus_handle: FocusHandle,
 32    _remote_id: Option<ViewId>,
 33    show_console_indicator: bool,
 34    module_list: Entity<module_list::ModuleList>,
 35    active_thread_item: ThreadItem,
 36    workspace: WeakEntity<Workspace>,
 37    session_id: SessionId,
 38    variable_list: Entity<variable_list::VariableList>,
 39    _subscriptions: Vec<Subscription>,
 40    stack_frame_list: Entity<stack_frame_list::StackFrameList>,
 41    loaded_source_list: Entity<loaded_source_list::LoadedSourceList>,
 42}
 43
 44impl Render for RunningState {
 45    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 46        let threads = self.session.update(cx, |this, cx| this.threads(cx));
 47        self.select_current_thread(&threads, cx);
 48
 49        let thread_status = self
 50            .thread_id
 51            .map(|thread_id| self.session.read(cx).thread_status(thread_id))
 52            .unwrap_or(ThreadStatus::Exited);
 53
 54        let selected_thread_name = threads
 55            .iter()
 56            .find(|(thread, _)| self.thread_id.map(|id| id.0) == Some(thread.id))
 57            .map(|(thread, _)| thread.name.clone())
 58            .unwrap_or("Threads".to_owned());
 59
 60        self.variable_list.update(cx, |this, cx| {
 61            this.disabled(thread_status != ThreadStatus::Stopped, cx);
 62        });
 63
 64        let is_terminated = self.session.read(cx).is_terminated();
 65        let active_thread_item = &self.active_thread_item;
 66
 67        let has_no_threads = threads.is_empty();
 68        let capabilities = self.capabilities(cx);
 69        let state = cx.entity();
 70        h_flex()
 71            .when(is_terminated, |this| this.bg(gpui::red()))
 72            .key_context("DebugPanelItem")
 73            .track_focus(&self.focus_handle(cx))
 74            .size_full()
 75            .items_start()
 76            .child(
 77                v_flex()
 78                    .size_full()
 79                    .items_start()
 80                    .child(
 81                        h_flex()
 82                            .w_full()
 83                            .border_b_1()
 84                            .border_color(cx.theme().colors().border_variant)
 85                            .justify_between()
 86                            .child(
 87                                h_flex()
 88                                    .p_1()
 89                                    .w_full()
 90                                    .gap_2()
 91                                    .map(|this| {
 92                                        if thread_status == ThreadStatus::Running {
 93                                            this.child(
 94                                                IconButton::new(
 95                                                    "debug-pause",
 96                                                    IconName::DebugPause,
 97                                                )
 98                                                .icon_size(IconSize::Small)
 99                                                .on_click(cx.listener(|this, _, _window, cx| {
100                                                    this.pause_thread(cx);
101                                                }))
102                                                .tooltip(move |window, cx| {
103                                                    Tooltip::text("Pause program")(window, cx)
104                                                }),
105                                            )
106                                        } else {
107                                            this.child(
108                                                IconButton::new(
109                                                    "debug-continue",
110                                                    IconName::DebugContinue,
111                                                )
112                                                .icon_size(IconSize::Small)
113                                                .on_click(cx.listener(|this, _, _window, cx| {
114                                                    this.continue_thread(cx)
115                                                }))
116                                                .disabled(thread_status != ThreadStatus::Stopped)
117                                                .tooltip(move |window, cx| {
118                                                    Tooltip::text("Continue program")(window, cx)
119                                                }),
120                                            )
121                                        }
122                                    })
123                                    .when(
124                                        capabilities.supports_step_back.unwrap_or(false),
125                                        |this| {
126                                            this.child(
127                                                IconButton::new(
128                                                    "debug-step-back",
129                                                    IconName::DebugStepBack,
130                                                )
131                                                .icon_size(IconSize::Small)
132                                                .on_click(cx.listener(|this, _, _window, cx| {
133                                                    this.step_back(cx);
134                                                }))
135                                                .disabled(thread_status != ThreadStatus::Stopped)
136                                                .tooltip(move |window, cx| {
137                                                    Tooltip::text("Step back")(window, cx)
138                                                }),
139                                            )
140                                        },
141                                    )
142                                    .child(
143                                        IconButton::new("debug-step-over", IconName::DebugStepOver)
144                                            .icon_size(IconSize::Small)
145                                            .on_click(cx.listener(|this, _, _window, cx| {
146                                                this.step_over(cx);
147                                            }))
148                                            .disabled(thread_status != ThreadStatus::Stopped)
149                                            .tooltip(move |window, cx| {
150                                                Tooltip::text("Step over")(window, cx)
151                                            }),
152                                    )
153                                    .child(
154                                        IconButton::new("debug-step-in", IconName::DebugStepInto)
155                                            .icon_size(IconSize::Small)
156                                            .on_click(cx.listener(|this, _, _window, cx| {
157                                                this.step_in(cx);
158                                            }))
159                                            .disabled(thread_status != ThreadStatus::Stopped)
160                                            .tooltip(move |window, cx| {
161                                                Tooltip::text("Step in")(window, cx)
162                                            }),
163                                    )
164                                    .child(
165                                        IconButton::new("debug-step-out", IconName::DebugStepOut)
166                                            .icon_size(IconSize::Small)
167                                            .on_click(cx.listener(|this, _, _window, cx| {
168                                                this.step_out(cx);
169                                            }))
170                                            .disabled(thread_status != ThreadStatus::Stopped)
171                                            .tooltip(move |window, cx| {
172                                                Tooltip::text("Step out")(window, cx)
173                                            }),
174                                    )
175                                    .child(
176                                        IconButton::new("debug-restart", IconName::DebugRestart)
177                                            .icon_size(IconSize::Small)
178                                            .on_click(cx.listener(|this, _, _window, cx| {
179                                                this.restart_session(cx);
180                                            }))
181                                            .disabled(
182                                                !capabilities
183                                                    .supports_restart_request
184                                                    .unwrap_or_default(),
185                                            )
186                                            .tooltip(move |window, cx| {
187                                                Tooltip::text("Restart")(window, cx)
188                                            }),
189                                    )
190                                    .child(
191                                        IconButton::new("debug-stop", IconName::DebugStop)
192                                            .icon_size(IconSize::Small)
193                                            .on_click(cx.listener(|this, _, _window, cx| {
194                                                this.stop_thread(cx);
195                                            }))
196                                            .disabled(
197                                                thread_status != ThreadStatus::Stopped
198                                                    && thread_status != ThreadStatus::Running,
199                                            )
200                                            .tooltip({
201                                                let label = if capabilities
202                                                    .supports_terminate_threads_request
203                                                    .unwrap_or_default()
204                                                {
205                                                    "Terminate Thread"
206                                                } else {
207                                                    "Terminate all Threads"
208                                                };
209                                                move |window, cx| Tooltip::text(label)(window, cx)
210                                            }),
211                                    )
212                                    .child(
213                                        IconButton::new(
214                                            "debug-disconnect",
215                                            IconName::DebugDisconnect,
216                                        )
217                                        .icon_size(IconSize::Small)
218                                        .on_click(cx.listener(|this, _, _window, cx| {
219                                            this.disconnect_client(cx);
220                                        }))
221                                        .disabled(
222                                            thread_status == ThreadStatus::Exited
223                                                || thread_status == ThreadStatus::Ended,
224                                        )
225                                        .tooltip(
226                                            move |window, cx| {
227                                                Tooltip::text("Disconnect")(window, cx)
228                                            },
229                                        ),
230                                    )
231                                    .child(
232                                        IconButton::new(
233                                            "debug-ignore-breakpoints",
234                                            if self.session.read(cx).breakpoints_enabled() {
235                                                IconName::DebugBreakpoint
236                                            } else {
237                                                IconName::DebugIgnoreBreakpoints
238                                            },
239                                        )
240                                        .icon_size(IconSize::Small)
241                                        .on_click(cx.listener(|this, _, _window, cx| {
242                                            this.toggle_ignore_breakpoints(cx);
243                                        }))
244                                        .disabled(
245                                            thread_status == ThreadStatus::Exited
246                                                || thread_status == ThreadStatus::Ended,
247                                        )
248                                        .tooltip(
249                                            move |window, cx| {
250                                                Tooltip::text("Ignore breakpoints")(window, cx)
251                                            },
252                                        ),
253                                    ),
254                            )
255                            //.child(h_flex())
256                            .child(
257                                h_flex().p_1().mx_2().w_3_4().justify_end().child(
258                                    DropdownMenu::new(
259                                        ("thread-list", self.session_id.0),
260                                        selected_thread_name,
261                                        ContextMenu::build(window, cx, move |mut this, _, _| {
262                                            for (thread, _) in threads {
263                                                let state = state.clone();
264                                                let thread_id = thread.id;
265                                                this =
266                                                    this.entry(thread.name, None, move |_, cx| {
267                                                        state.update(cx, |state, cx| {
268                                                            state.select_thread(
269                                                                ThreadId(thread_id),
270                                                                cx,
271                                                            );
272                                                        });
273                                                    });
274                                            }
275                                            this
276                                        }),
277                                    )
278                                    .disabled(
279                                        has_no_threads || thread_status != ThreadStatus::Stopped,
280                                    ),
281                                ),
282                            ),
283                    )
284                    .child(
285                        h_flex()
286                            .size_full()
287                            .items_start()
288                            .p_1()
289                            .gap_4()
290                            .child(self.stack_frame_list.clone()),
291                    ),
292            )
293            .child(
294                v_flex()
295                    .border_l_1()
296                    .border_color(cx.theme().colors().border_variant)
297                    .size_full()
298                    .items_start()
299                    .child(
300                        h_flex()
301                            .border_b_1()
302                            .w_full()
303                            .border_color(cx.theme().colors().border_variant)
304                            .child(self.render_entry_button(
305                                &SharedString::from("Variables"),
306                                ThreadItem::Variables,
307                                cx,
308                            ))
309                            .when(
310                                capabilities.supports_modules_request.unwrap_or_default(),
311                                |this| {
312                                    this.child(self.render_entry_button(
313                                        &SharedString::from("Modules"),
314                                        ThreadItem::Modules,
315                                        cx,
316                                    ))
317                                },
318                            )
319                            .when(
320                                capabilities
321                                    .supports_loaded_sources_request
322                                    .unwrap_or_default(),
323                                |this| {
324                                    this.child(self.render_entry_button(
325                                        &SharedString::from("Loaded Sources"),
326                                        ThreadItem::LoadedSource,
327                                        cx,
328                                    ))
329                                },
330                            )
331                            .child(self.render_entry_button(
332                                &SharedString::from("Console"),
333                                ThreadItem::Console,
334                                cx,
335                            )),
336                    )
337                    .when(*active_thread_item == ThreadItem::Variables, |this| {
338                        this.child(self.variable_list.clone())
339                    })
340                    .when(*active_thread_item == ThreadItem::Modules, |this| {
341                        this.size_full().child(self.module_list.clone())
342                    })
343                    .when(*active_thread_item == ThreadItem::LoadedSource, |this| {
344                        this.size_full().child(self.loaded_source_list.clone())
345                    })
346                    .when(*active_thread_item == ThreadItem::Console, |this| {
347                        this.child(self.console.clone())
348                    }),
349            )
350    }
351}
352
353impl RunningState {
354    pub fn new(
355        session: Entity<Session>,
356        workspace: WeakEntity<Workspace>,
357        window: &mut Window,
358        cx: &mut Context<Self>,
359    ) -> Self {
360        let focus_handle = cx.focus_handle();
361        let session_id = session.read(cx).session_id();
362        let weak_state = cx.weak_entity();
363        let stack_frame_list = cx.new(|cx| {
364            StackFrameList::new(workspace.clone(), session.clone(), weak_state, window, cx)
365        });
366
367        let variable_list =
368            cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx));
369
370        let module_list = cx.new(|cx| ModuleList::new(session.clone(), workspace.clone(), cx));
371
372        let loaded_source_list = cx.new(|cx| LoadedSourceList::new(session.clone(), cx));
373
374        let console = cx.new(|cx| {
375            Console::new(
376                session.clone(),
377                stack_frame_list.clone(),
378                variable_list.clone(),
379                window,
380                cx,
381            )
382        });
383
384        let _subscriptions = vec![
385            cx.observe(&module_list, |_, _, cx| cx.notify()),
386            cx.subscribe_in(&session, window, |this, _, event, window, cx| {
387                match event {
388                    SessionEvent::Stopped(thread_id) => {
389                        this.workspace
390                            .update(cx, |workspace, cx| {
391                                workspace.open_panel::<crate::DebugPanel>(window, cx);
392                            })
393                            .log_err();
394
395                        if let Some(thread_id) = thread_id {
396                            this.select_thread(*thread_id, cx);
397                        }
398                    }
399                    SessionEvent::Threads => {
400                        let threads = this.session.update(cx, |this, cx| this.threads(cx));
401                        this.select_current_thread(&threads, cx);
402                    }
403                    _ => {}
404                }
405                cx.notify()
406            }),
407        ];
408
409        Self {
410            session,
411            console,
412            workspace,
413            module_list,
414            focus_handle,
415            variable_list,
416            _subscriptions,
417            thread_id: None,
418            _remote_id: None,
419            stack_frame_list,
420            loaded_source_list,
421            session_id,
422            show_console_indicator: false,
423            active_thread_item: ThreadItem::Variables,
424        }
425    }
426
427    pub(crate) fn go_to_selected_stack_frame(&self, window: &Window, cx: &mut Context<Self>) {
428        if self.thread_id.is_some() {
429            self.stack_frame_list
430                .update(cx, |list, cx| list.go_to_selected_stack_frame(window, cx));
431        }
432    }
433
434    pub fn session(&self) -> &Entity<Session> {
435        &self.session
436    }
437
438    pub fn session_id(&self) -> SessionId {
439        self.session_id
440    }
441
442    #[cfg(any(test, feature = "test-support"))]
443    pub fn set_thread_item(&mut self, thread_item: ThreadItem, cx: &mut Context<Self>) {
444        self.active_thread_item = thread_item;
445        cx.notify()
446    }
447
448    #[cfg(any(test, feature = "test-support"))]
449    pub fn stack_frame_list(&self) -> &Entity<StackFrameList> {
450        &self.stack_frame_list
451    }
452
453    #[cfg(any(test, feature = "test-support"))]
454    pub fn console(&self) -> &Entity<Console> {
455        &self.console
456    }
457
458    #[cfg(any(test, feature = "test-support"))]
459    pub fn module_list(&self) -> &Entity<ModuleList> {
460        &self.module_list
461    }
462
463    #[cfg(any(test, feature = "test-support"))]
464    pub fn variable_list(&self) -> &Entity<VariableList> {
465        &self.variable_list
466    }
467
468    #[cfg(any(test, feature = "test-support"))]
469    pub fn are_breakpoints_ignored(&self, cx: &App) -> bool {
470        self.session.read(cx).ignore_breakpoints()
471    }
472
473    pub fn capabilities(&self, cx: &App) -> Capabilities {
474        self.session().read(cx).capabilities().clone()
475    }
476
477    pub fn select_current_thread(
478        &mut self,
479        threads: &Vec<(Thread, ThreadStatus)>,
480        cx: &mut Context<Self>,
481    ) {
482        let selected_thread = self
483            .thread_id
484            .and_then(|thread_id| threads.iter().find(|(thread, _)| thread.id == thread_id.0))
485            .or_else(|| threads.first());
486
487        let Some((selected_thread, _)) = selected_thread else {
488            return;
489        };
490
491        if Some(ThreadId(selected_thread.id)) != self.thread_id {
492            self.select_thread(ThreadId(selected_thread.id), cx);
493        }
494    }
495
496    #[cfg(any(test, feature = "test-support"))]
497    pub fn selected_thread_id(&self) -> Option<ThreadId> {
498        self.thread_id
499    }
500
501    pub fn thread_status(&self, cx: &App) -> Option<ThreadStatus> {
502        self.thread_id
503            .map(|id| self.session().read(cx).thread_status(id))
504    }
505
506    fn select_thread(&mut self, thread_id: ThreadId, cx: &mut Context<Self>) {
507        if self.thread_id.is_some_and(|id| id == thread_id) {
508            return;
509        }
510
511        self.thread_id = Some(thread_id);
512
513        self.stack_frame_list
514            .update(cx, |list, cx| list.refresh(cx));
515        cx.notify();
516    }
517
518    fn render_entry_button(
519        &self,
520        label: &SharedString,
521        thread_item: ThreadItem,
522        cx: &mut Context<Self>,
523    ) -> AnyElement {
524        let has_indicator =
525            matches!(thread_item, ThreadItem::Console) && self.show_console_indicator;
526
527        div()
528            .id(label.clone())
529            .px_2()
530            .py_1()
531            .cursor_pointer()
532            .border_b_2()
533            .when(self.active_thread_item == thread_item, |this| {
534                this.border_color(cx.theme().colors().border)
535            })
536            .child(
537                h_flex()
538                    .child(Button::new(label.clone(), label.clone()))
539                    .when(has_indicator, |this| this.child(Indicator::dot())),
540            )
541            .on_click(cx.listener(move |this, _, _window, cx| {
542                this.active_thread_item = thread_item;
543
544                if matches!(this.active_thread_item, ThreadItem::Console) {
545                    this.show_console_indicator = false;
546                }
547
548                cx.notify();
549            }))
550            .into_any_element()
551    }
552
553    pub fn continue_thread(&mut self, cx: &mut Context<Self>) {
554        let Some(thread_id) = self.thread_id else {
555            return;
556        };
557
558        self.session().update(cx, |state, cx| {
559            state.continue_thread(thread_id, cx);
560        });
561    }
562
563    pub fn step_over(&mut self, cx: &mut Context<Self>) {
564        let Some(thread_id) = self.thread_id else {
565            return;
566        };
567
568        let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
569
570        self.session().update(cx, |state, cx| {
571            state.step_over(thread_id, granularity, cx);
572        });
573    }
574
575    pub fn step_in(&mut self, cx: &mut Context<Self>) {
576        let Some(thread_id) = self.thread_id else {
577            return;
578        };
579
580        let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
581
582        self.session().update(cx, |state, cx| {
583            state.step_in(thread_id, granularity, cx);
584        });
585    }
586
587    pub fn step_out(&mut self, cx: &mut Context<Self>) {
588        let Some(thread_id) = self.thread_id else {
589            return;
590        };
591
592        let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
593
594        self.session().update(cx, |state, cx| {
595            state.step_out(thread_id, granularity, cx);
596        });
597    }
598
599    pub fn step_back(&mut self, cx: &mut Context<Self>) {
600        let Some(thread_id) = self.thread_id else {
601            return;
602        };
603
604        let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
605
606        self.session().update(cx, |state, cx| {
607            state.step_back(thread_id, granularity, cx);
608        });
609    }
610
611    pub fn restart_session(&self, cx: &mut Context<Self>) {
612        self.session().update(cx, |state, cx| {
613            state.restart(None, cx);
614        });
615    }
616
617    pub fn pause_thread(&self, cx: &mut Context<Self>) {
618        let Some(thread_id) = self.thread_id else {
619            return;
620        };
621
622        self.session().update(cx, |state, cx| {
623            state.pause_thread(thread_id, cx);
624        });
625    }
626
627    pub(crate) fn shutdown(&mut self, cx: &mut Context<Self>) {
628        self.workspace
629            .update(cx, |workspace, cx| {
630                workspace
631                    .project()
632                    .read(cx)
633                    .breakpoint_store()
634                    .update(cx, |store, cx| {
635                        store.remove_active_position(Some(self.session_id), cx)
636                    })
637            })
638            .log_err();
639
640        self.session.update(cx, |session, cx| {
641            session.shutdown(cx).detach();
642        })
643    }
644
645    pub fn stop_thread(&self, cx: &mut Context<Self>) {
646        let Some(thread_id) = self.thread_id else {
647            return;
648        };
649
650        self.workspace
651            .update(cx, |workspace, cx| {
652                workspace
653                    .project()
654                    .read(cx)
655                    .breakpoint_store()
656                    .update(cx, |store, cx| {
657                        store.remove_active_position(Some(self.session_id), cx)
658                    })
659            })
660            .log_err();
661
662        self.session().update(cx, |state, cx| {
663            state.terminate_threads(Some(vec![thread_id; 1]), cx);
664        });
665    }
666
667    pub fn disconnect_client(&self, cx: &mut Context<Self>) {
668        self.session().update(cx, |state, cx| {
669            state.disconnect_client(cx);
670        });
671    }
672
673    pub fn toggle_ignore_breakpoints(&mut self, cx: &mut Context<Self>) {
674        self.session.update(cx, |session, cx| {
675            session.toggle_ignore_breakpoints(cx).detach();
676        });
677    }
678}
679
680impl EventEmitter<DebugPanelItemEvent> for RunningState {}
681
682impl Focusable for RunningState {
683    fn focus_handle(&self, _: &App) -> FocusHandle {
684        self.focus_handle.clone()
685    }
686}