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