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