1use dap::{
2 adapters::DebugAdapterName,
3 client::SessionId,
4 debugger_settings::DebuggerSettings,
5 transport::{IoKind, LogKind},
6};
7use editor::{Editor, EditorEvent};
8use futures::{
9 StreamExt,
10 channel::mpsc::{UnboundedSender, unbounded},
11};
12use gpui::{
13 App, AppContext, Context, Empty, Entity, EventEmitter, FocusHandle, Focusable, IntoElement,
14 ParentElement, Render, SharedString, Styled, Subscription, WeakEntity, Window, actions, div,
15};
16use project::{
17 Project,
18 debugger::{dap_store, session::Session},
19 search::SearchQuery,
20};
21use settings::Settings as _;
22use std::{
23 borrow::Cow,
24 collections::{BTreeMap, HashMap, VecDeque},
25 sync::Arc,
26};
27use util::maybe;
28use workspace::{
29 ToolbarItemEvent, ToolbarItemView, Workspace,
30 item::Item,
31 searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
32 ui::{Button, Clickable, ContextMenu, Label, LabelCommon, PopoverMenu, h_flex},
33};
34
35#[derive(Debug, Copy, Clone, PartialEq, Eq)]
36enum View {
37 AdapterLogs,
38 RpcMessages,
39 InitializationSequence,
40}
41
42struct DapLogView {
43 editor: Entity<Editor>,
44 focus_handle: FocusHandle,
45 log_store: Entity<LogStore>,
46 editor_subscriptions: Vec<Subscription>,
47 current_view: Option<(SessionId, View)>,
48 project: Entity<Project>,
49 _subscriptions: Vec<Subscription>,
50}
51
52struct LogStoreEntryIdentifier<'a> {
53 session_id: SessionId,
54 project: Cow<'a, WeakEntity<Project>>,
55}
56impl LogStoreEntryIdentifier<'_> {
57 fn to_owned(&self) -> LogStoreEntryIdentifier<'static> {
58 LogStoreEntryIdentifier {
59 session_id: self.session_id,
60 project: Cow::Owned(self.project.as_ref().clone()),
61 }
62 }
63}
64
65struct LogStoreMessage {
66 id: LogStoreEntryIdentifier<'static>,
67 kind: IoKind,
68 command: Option<SharedString>,
69 message: SharedString,
70}
71
72pub struct LogStore {
73 projects: HashMap<WeakEntity<Project>, ProjectState>,
74 rpc_tx: UnboundedSender<LogStoreMessage>,
75 adapter_log_tx: UnboundedSender<LogStoreMessage>,
76}
77
78struct ProjectState {
79 debug_sessions: BTreeMap<SessionId, DebugAdapterState>,
80 _subscriptions: [gpui::Subscription; 2],
81}
82
83struct DebugAdapterState {
84 id: SessionId,
85 log_messages: VecDeque<SharedString>,
86 rpc_messages: RpcMessages,
87 session_label: SharedString,
88 adapter_name: DebugAdapterName,
89 has_adapter_logs: bool,
90 is_terminated: bool,
91}
92
93struct RpcMessages {
94 messages: VecDeque<SharedString>,
95 last_message_kind: Option<MessageKind>,
96 initialization_sequence: Vec<SharedString>,
97 last_init_message_kind: Option<MessageKind>,
98}
99
100impl RpcMessages {
101 const MESSAGE_QUEUE_LIMIT: usize = 255;
102
103 fn new() -> Self {
104 Self {
105 last_message_kind: None,
106 last_init_message_kind: None,
107 messages: VecDeque::with_capacity(Self::MESSAGE_QUEUE_LIMIT),
108 initialization_sequence: Vec::new(),
109 }
110 }
111}
112
113const SEND: &str = "// Send";
114const RECEIVE: &str = "// Receive";
115
116#[derive(Clone, Copy, PartialEq, Eq)]
117enum MessageKind {
118 Send,
119 Receive,
120}
121
122impl MessageKind {
123 fn label(&self) -> &'static str {
124 match self {
125 Self::Send => SEND,
126 Self::Receive => RECEIVE,
127 }
128 }
129}
130
131impl DebugAdapterState {
132 fn new(
133 id: SessionId,
134 adapter_name: DebugAdapterName,
135 session_label: SharedString,
136 has_adapter_logs: bool,
137 ) -> Self {
138 Self {
139 id,
140 log_messages: VecDeque::new(),
141 rpc_messages: RpcMessages::new(),
142 adapter_name,
143 session_label,
144 has_adapter_logs,
145 is_terminated: false,
146 }
147 }
148}
149
150impl LogStore {
151 pub fn new(cx: &Context<Self>) -> Self {
152 let (rpc_tx, mut rpc_rx) = unbounded::<LogStoreMessage>();
153 cx.spawn(async move |this, cx| {
154 while let Some(message) = rpc_rx.next().await {
155 if let Some(this) = this.upgrade() {
156 this.update(cx, |this, cx| {
157 this.add_debug_adapter_message(message, cx);
158 })?;
159 }
160
161 smol::future::yield_now().await;
162 }
163 anyhow::Ok(())
164 })
165 .detach_and_log_err(cx);
166
167 let (adapter_log_tx, mut adapter_log_rx) = unbounded::<LogStoreMessage>();
168 cx.spawn(async move |this, cx| {
169 while let Some(message) = adapter_log_rx.next().await {
170 if let Some(this) = this.upgrade() {
171 this.update(cx, |this, cx| {
172 this.add_debug_adapter_log(message, cx);
173 })?;
174 }
175
176 smol::future::yield_now().await;
177 }
178 anyhow::Ok(())
179 })
180 .detach_and_log_err(cx);
181 Self {
182 rpc_tx,
183 adapter_log_tx,
184 projects: HashMap::new(),
185 }
186 }
187
188 pub fn add_project(&mut self, project: &Entity<Project>, cx: &mut Context<Self>) {
189 self.projects.insert(
190 project.downgrade(),
191 ProjectState {
192 _subscriptions: [
193 cx.observe_release(project, {
194 let weak_project = project.downgrade();
195 move |this, _, _| {
196 this.projects.remove(&weak_project);
197 }
198 }),
199 cx.subscribe(&project.read(cx).dap_store(), {
200 let weak_project = project.downgrade();
201 move |this, dap_store, event, cx| match event {
202 dap_store::DapStoreEvent::DebugClientStarted(session_id) => {
203 let session = dap_store.read(cx).session_by_id(session_id);
204 if let Some(session) = session {
205 this.add_debug_session(
206 LogStoreEntryIdentifier {
207 project: Cow::Owned(weak_project.clone()),
208 session_id: *session_id,
209 },
210 session,
211 cx,
212 );
213 }
214 }
215 dap_store::DapStoreEvent::DebugClientShutdown(session_id) => {
216 let id = LogStoreEntryIdentifier {
217 project: Cow::Borrowed(&weak_project),
218 session_id: *session_id,
219 };
220 if let Some(state) = this.get_debug_adapter_state(&id) {
221 state.is_terminated = true;
222 }
223
224 this.clean_sessions(cx);
225 }
226 _ => {}
227 }
228 }),
229 ],
230 debug_sessions: Default::default(),
231 },
232 );
233 }
234
235 fn get_debug_adapter_state(
236 &mut self,
237 id: &LogStoreEntryIdentifier<'_>,
238 ) -> Option<&mut DebugAdapterState> {
239 self.projects
240 .get_mut(&id.project)
241 .and_then(|state| state.debug_sessions.get_mut(&id.session_id))
242 }
243
244 fn add_debug_adapter_message(
245 &mut self,
246 LogStoreMessage {
247 id,
248 kind: io_kind,
249 command,
250 message,
251 }: LogStoreMessage,
252 cx: &mut Context<Self>,
253 ) {
254 let Some(debug_client_state) = self.get_debug_adapter_state(&id) else {
255 return;
256 };
257
258 let is_init_seq = command.as_ref().is_some_and(|command| {
259 matches!(
260 command.as_ref(),
261 "attach" | "launch" | "initialize" | "configurationDone"
262 )
263 });
264
265 let kind = match io_kind {
266 IoKind::StdOut | IoKind::StdErr => MessageKind::Receive,
267 IoKind::StdIn => MessageKind::Send,
268 };
269
270 let rpc_messages = &mut debug_client_state.rpc_messages;
271
272 // Push a separator if the kind has changed
273 if rpc_messages.last_message_kind != Some(kind) {
274 Self::get_debug_adapter_entry(
275 &mut rpc_messages.messages,
276 id.to_owned(),
277 kind.label().into(),
278 LogKind::Rpc,
279 cx,
280 );
281 rpc_messages.last_message_kind = Some(kind);
282 }
283
284 let entry = Self::get_debug_adapter_entry(
285 &mut rpc_messages.messages,
286 id.to_owned(),
287 message,
288 LogKind::Rpc,
289 cx,
290 );
291
292 if is_init_seq {
293 if rpc_messages.last_init_message_kind != Some(kind) {
294 rpc_messages
295 .initialization_sequence
296 .push(SharedString::from(kind.label()));
297 rpc_messages.last_init_message_kind = Some(kind);
298 }
299 rpc_messages.initialization_sequence.push(entry);
300 }
301
302 cx.notify();
303 }
304
305 fn add_debug_adapter_log(
306 &mut self,
307 LogStoreMessage {
308 id,
309 kind: io_kind,
310 message,
311 ..
312 }: LogStoreMessage,
313 cx: &mut Context<Self>,
314 ) {
315 let Some(debug_adapter_state) = self.get_debug_adapter_state(&id) else {
316 return;
317 };
318
319 let message = match io_kind {
320 IoKind::StdErr => format!("stderr: {message}").into(),
321 _ => message,
322 };
323
324 Self::get_debug_adapter_entry(
325 &mut debug_adapter_state.log_messages,
326 id.to_owned(),
327 message,
328 LogKind::Adapter,
329 cx,
330 );
331 cx.notify();
332 }
333
334 fn get_debug_adapter_entry(
335 log_lines: &mut VecDeque<SharedString>,
336 id: LogStoreEntryIdentifier<'static>,
337 message: SharedString,
338 kind: LogKind,
339 cx: &mut Context<Self>,
340 ) -> SharedString {
341 if let Some(excess) = log_lines
342 .len()
343 .checked_sub(RpcMessages::MESSAGE_QUEUE_LIMIT)
344 && excess > 0
345 {
346 log_lines.drain(..excess);
347 }
348
349 let format_messages = DebuggerSettings::get_global(cx).format_dap_log_messages;
350
351 let entry = if format_messages {
352 maybe!({
353 serde_json::to_string_pretty::<serde_json::Value>(
354 &serde_json::from_str(&message).ok()?,
355 )
356 .ok()
357 })
358 .map(SharedString::from)
359 .unwrap_or(message)
360 } else {
361 message
362 };
363 log_lines.push_back(entry.clone());
364
365 cx.emit(Event::NewLogEntry {
366 id,
367 entry: entry.clone(),
368 kind,
369 });
370
371 entry
372 }
373
374 fn add_debug_session(
375 &mut self,
376 id: LogStoreEntryIdentifier<'static>,
377 session: Entity<Session>,
378 cx: &mut Context<Self>,
379 ) {
380 maybe!({
381 let project_entry = self.projects.get_mut(&id.project)?;
382 let std::collections::btree_map::Entry::Vacant(state) =
383 project_entry.debug_sessions.entry(id.session_id)
384 else {
385 return None;
386 };
387
388 let (adapter_name, session_label, has_adapter_logs) =
389 session.read_with(cx, |session, _| {
390 (
391 session.adapter(),
392 session.label(),
393 session
394 .adapter_client()
395 .map_or(false, |client| client.has_adapter_logs()),
396 )
397 });
398
399 state.insert(DebugAdapterState::new(
400 id.session_id,
401 adapter_name,
402 session_label,
403 has_adapter_logs,
404 ));
405
406 self.clean_sessions(cx);
407
408 let io_tx = self.rpc_tx.clone();
409
410 let client = session.read(cx).adapter_client()?;
411 let project = id.project.clone();
412 let session_id = id.session_id;
413 client.add_log_handler(
414 move |kind, command, message| {
415 io_tx
416 .unbounded_send(LogStoreMessage {
417 id: LogStoreEntryIdentifier {
418 session_id,
419 project: project.clone(),
420 },
421 kind,
422 command: command.map(|command| command.to_owned().into()),
423 message: message.to_owned().into(),
424 })
425 .ok();
426 },
427 LogKind::Rpc,
428 );
429
430 let log_io_tx = self.adapter_log_tx.clone();
431 let project = id.project;
432 client.add_log_handler(
433 move |kind, command, message| {
434 log_io_tx
435 .unbounded_send(LogStoreMessage {
436 id: LogStoreEntryIdentifier {
437 session_id,
438 project: project.clone(),
439 },
440 kind,
441 command: command.map(|command| command.to_owned().into()),
442 message: message.to_owned().into(),
443 })
444 .ok();
445 },
446 LogKind::Adapter,
447 );
448 Some(())
449 });
450 }
451
452 fn clean_sessions(&mut self, cx: &mut Context<Self>) {
453 self.projects.values_mut().for_each(|project| {
454 let mut allowed_terminated_sessions = 10u32;
455 project.debug_sessions.retain(|_, session| {
456 if !session.is_terminated {
457 return true;
458 }
459 allowed_terminated_sessions = allowed_terminated_sessions.saturating_sub(1);
460 allowed_terminated_sessions > 0
461 });
462 });
463
464 cx.notify();
465 }
466
467 fn log_messages_for_session(
468 &mut self,
469 id: &LogStoreEntryIdentifier<'_>,
470 ) -> Option<&mut VecDeque<SharedString>> {
471 self.get_debug_adapter_state(id)
472 .map(|state| &mut state.log_messages)
473 }
474
475 fn rpc_messages_for_session(
476 &mut self,
477 id: &LogStoreEntryIdentifier<'_>,
478 ) -> Option<&mut VecDeque<SharedString>> {
479 self.get_debug_adapter_state(id)
480 .map(|state| &mut state.rpc_messages.messages)
481 }
482
483 fn initialization_sequence_for_session(
484 &mut self,
485 id: &LogStoreEntryIdentifier<'_>,
486 ) -> Option<&Vec<SharedString>> {
487 self.get_debug_adapter_state(&id)
488 .map(|state| &state.rpc_messages.initialization_sequence)
489 }
490}
491
492pub struct DapLogToolbarItemView {
493 log_view: Option<Entity<DapLogView>>,
494}
495
496impl DapLogToolbarItemView {
497 pub fn new() -> Self {
498 Self { log_view: None }
499 }
500}
501
502impl Render for DapLogToolbarItemView {
503 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
504 let Some(log_view) = self.log_view.clone() else {
505 return Empty.into_any_element();
506 };
507
508 let (menu_rows, current_session_id, project) = log_view.update(cx, |log_view, cx| {
509 (
510 log_view.menu_items(cx),
511 log_view.current_view.map(|(session_id, _)| session_id),
512 log_view.project.downgrade(),
513 )
514 });
515
516 let current_client = current_session_id
517 .and_then(|session_id| menu_rows.iter().find(|row| row.session_id == session_id));
518
519 let dap_menu: PopoverMenu<_> = PopoverMenu::new("DapLogView")
520 .anchor(gpui::Corner::TopLeft)
521 .trigger(Button::new(
522 "debug_client_menu_header",
523 current_client
524 .map(|sub_item| {
525 Cow::Owned(format!(
526 "{} - {} - {}",
527 sub_item.adapter_name,
528 sub_item.session_label,
529 match sub_item.selected_entry {
530 View::AdapterLogs => ADAPTER_LOGS,
531 View::RpcMessages => RPC_MESSAGES,
532 View::InitializationSequence => INITIALIZATION_SEQUENCE,
533 }
534 ))
535 })
536 .unwrap_or_else(|| "No adapter selected".into()),
537 ))
538 .menu(move |mut window, cx| {
539 let log_view = log_view.clone();
540 let menu_rows = menu_rows.clone();
541 let project = project.clone();
542 ContextMenu::build(&mut window, cx, move |mut menu, window, _cx| {
543 for row in menu_rows.into_iter() {
544 menu = menu.custom_row(move |_window, _cx| {
545 div()
546 .w_full()
547 .pl_2()
548 .child(
549 Label::new(format!(
550 "{} - {}",
551 row.adapter_name, row.session_label
552 ))
553 .color(workspace::ui::Color::Muted),
554 )
555 .into_any_element()
556 });
557
558 if row.has_adapter_logs {
559 menu = menu.custom_entry(
560 move |_window, _cx| {
561 div()
562 .w_full()
563 .pl_4()
564 .child(Label::new(ADAPTER_LOGS))
565 .into_any_element()
566 },
567 window.handler_for(&log_view, {
568 let project = project.clone();
569 let id = LogStoreEntryIdentifier {
570 project: Cow::Owned(project),
571 session_id: row.session_id,
572 };
573 move |view, window, cx| {
574 view.show_log_messages_for_adapter(&id, window, cx);
575 }
576 }),
577 );
578 }
579
580 menu = menu
581 .custom_entry(
582 move |_window, _cx| {
583 div()
584 .w_full()
585 .pl_4()
586 .child(Label::new(RPC_MESSAGES))
587 .into_any_element()
588 },
589 window.handler_for(&log_view, {
590 let project = project.clone();
591 let id = LogStoreEntryIdentifier {
592 project: Cow::Owned(project),
593 session_id: row.session_id,
594 };
595 move |view, window, cx| {
596 view.show_rpc_trace_for_server(&id, window, cx);
597 }
598 }),
599 )
600 .custom_entry(
601 move |_window, _cx| {
602 div()
603 .w_full()
604 .pl_4()
605 .child(Label::new(INITIALIZATION_SEQUENCE))
606 .into_any_element()
607 },
608 window.handler_for(&log_view, {
609 let project = project.clone();
610 let id = LogStoreEntryIdentifier {
611 project: Cow::Owned(project),
612 session_id: row.session_id,
613 };
614 move |view, window, cx| {
615 view.show_initialization_sequence_for_server(
616 &id, window, cx,
617 );
618 }
619 }),
620 );
621 }
622
623 menu
624 })
625 .into()
626 });
627
628 h_flex()
629 .size_full()
630 .child(dap_menu)
631 .child(
632 div()
633 .child(
634 Button::new("clear_log_button", "Clear").on_click(cx.listener(
635 |this, _, window, cx| {
636 if let Some(log_view) = this.log_view.as_ref() {
637 log_view.update(cx, |log_view, cx| {
638 log_view.editor.update(cx, |editor, cx| {
639 editor.set_read_only(false);
640 editor.clear(window, cx);
641 editor.set_read_only(true);
642 });
643 })
644 }
645 },
646 )),
647 )
648 .ml_2(),
649 )
650 .into_any_element()
651 }
652}
653
654impl EventEmitter<ToolbarItemEvent> for DapLogToolbarItemView {}
655
656impl ToolbarItemView for DapLogToolbarItemView {
657 fn set_active_pane_item(
658 &mut self,
659 active_pane_item: Option<&dyn workspace::item::ItemHandle>,
660 _window: &mut Window,
661 cx: &mut Context<Self>,
662 ) -> workspace::ToolbarItemLocation {
663 if let Some(item) = active_pane_item {
664 if let Some(log_view) = item.downcast::<DapLogView>() {
665 self.log_view = Some(log_view.clone());
666 return workspace::ToolbarItemLocation::PrimaryLeft;
667 }
668 }
669 self.log_view = None;
670
671 cx.notify();
672
673 workspace::ToolbarItemLocation::Hidden
674 }
675}
676
677impl DapLogView {
678 pub fn new(
679 project: Entity<Project>,
680 log_store: Entity<LogStore>,
681 window: &mut Window,
682 cx: &mut Context<Self>,
683 ) -> Self {
684 let (editor, editor_subscriptions) = Self::editor_for_logs(String::new(), window, cx);
685
686 let focus_handle = cx.focus_handle();
687
688 let events_subscriptions = cx.subscribe(&log_store, |log_view, _, event, cx| match event {
689 Event::NewLogEntry { id, entry, kind } => {
690 let is_current_view = match (log_view.current_view, *kind) {
691 (Some((i, View::AdapterLogs)), LogKind::Adapter)
692 | (Some((i, View::RpcMessages)), LogKind::Rpc)
693 if i == id.session_id =>
694 {
695 log_view.project == *id.project
696 }
697 _ => false,
698 };
699 if is_current_view {
700 log_view.editor.update(cx, |editor, cx| {
701 editor.set_read_only(false);
702 let last_point = editor.buffer().read(cx).len(cx);
703 editor.edit(
704 vec![
705 (last_point..last_point, entry.trim()),
706 (last_point..last_point, "\n"),
707 ],
708 cx,
709 );
710 editor.set_read_only(true);
711 });
712 }
713 }
714 });
715 let weak_project = project.downgrade();
716 let state_info = log_store
717 .read(cx)
718 .projects
719 .get(&weak_project)
720 .and_then(|project| {
721 project
722 .debug_sessions
723 .values()
724 .next_back()
725 .map(|session| (session.id, session.has_adapter_logs))
726 });
727
728 let mut this = Self {
729 editor,
730 focus_handle,
731 project,
732 log_store,
733 editor_subscriptions,
734 current_view: None,
735 _subscriptions: vec![events_subscriptions],
736 };
737
738 if let Some((session_id, have_adapter_logs)) = state_info {
739 let id = LogStoreEntryIdentifier {
740 session_id,
741 project: Cow::Owned(weak_project),
742 };
743 if have_adapter_logs {
744 this.show_log_messages_for_adapter(&id, window, cx);
745 } else {
746 this.show_rpc_trace_for_server(&id, window, cx);
747 }
748 }
749
750 this
751 }
752
753 fn editor_for_logs(
754 log_contents: String,
755 window: &mut Window,
756 cx: &mut Context<Self>,
757 ) -> (Entity<Editor>, Vec<Subscription>) {
758 let editor = cx.new(|cx| {
759 let mut editor = Editor::multi_line(window, cx);
760 editor.set_text(log_contents, window, cx);
761 editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
762 editor.set_show_code_actions(false, cx);
763 editor.set_show_breakpoints(false, cx);
764 editor.set_show_git_diff_gutter(false, cx);
765 editor.set_show_runnables(false, cx);
766 editor.set_input_enabled(false);
767 editor.set_use_autoclose(false);
768 editor.set_read_only(true);
769 editor.set_show_edit_predictions(Some(false), window, cx);
770 editor
771 });
772 let editor_subscription = cx.subscribe(
773 &editor,
774 |_, _, event: &EditorEvent, cx: &mut Context<DapLogView>| cx.emit(event.clone()),
775 );
776 let search_subscription = cx.subscribe(
777 &editor,
778 |_, _, event: &SearchEvent, cx: &mut Context<DapLogView>| cx.emit(event.clone()),
779 );
780 (editor, vec![editor_subscription, search_subscription])
781 }
782
783 fn menu_items(&self, cx: &App) -> Vec<DapMenuItem> {
784 self.log_store
785 .read(cx)
786 .projects
787 .get(&self.project.downgrade())
788 .map_or_else(Vec::new, |state| {
789 state
790 .debug_sessions
791 .values()
792 .rev()
793 .map(|state| DapMenuItem {
794 session_id: state.id,
795 adapter_name: state.adapter_name.clone(),
796 session_label: state.session_label.clone(),
797 has_adapter_logs: state.has_adapter_logs,
798 selected_entry: self
799 .current_view
800 .map_or(View::AdapterLogs, |(_, kind)| kind),
801 })
802 .collect::<Vec<_>>()
803 })
804 }
805
806 fn show_rpc_trace_for_server(
807 &mut self,
808 id: &LogStoreEntryIdentifier<'_>,
809 window: &mut Window,
810 cx: &mut Context<Self>,
811 ) {
812 let rpc_log = self.log_store.update(cx, |log_store, _| {
813 log_store
814 .rpc_messages_for_session(id)
815 .map(|state| log_contents(state.iter().cloned()))
816 });
817 if let Some(rpc_log) = rpc_log {
818 self.current_view = Some((id.session_id, View::RpcMessages));
819 let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
820 let language = self.project.read(cx).languages().language_for_name("JSON");
821 editor
822 .read(cx)
823 .buffer()
824 .read(cx)
825 .as_singleton()
826 .expect("log buffer should be a singleton")
827 .update(cx, |_, cx| {
828 cx.spawn({
829 async move |buffer, cx| {
830 let language = language.await.ok();
831 buffer.update(cx, |buffer, cx| {
832 buffer.set_language(language, cx);
833 })
834 }
835 })
836 .detach_and_log_err(cx);
837 });
838
839 self.editor = editor;
840 self.editor_subscriptions = editor_subscriptions;
841 cx.notify();
842 }
843
844 cx.focus_self(window);
845 }
846
847 fn show_log_messages_for_adapter(
848 &mut self,
849 id: &LogStoreEntryIdentifier<'_>,
850 window: &mut Window,
851 cx: &mut Context<Self>,
852 ) {
853 let message_log = self.log_store.update(cx, |log_store, _| {
854 log_store
855 .log_messages_for_session(id)
856 .map(|state| log_contents(state.iter().cloned()))
857 });
858 if let Some(message_log) = message_log {
859 self.current_view = Some((id.session_id, View::AdapterLogs));
860 let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, window, cx);
861 editor
862 .read(cx)
863 .buffer()
864 .read(cx)
865 .as_singleton()
866 .expect("log buffer should be a singleton");
867
868 self.editor = editor;
869 self.editor_subscriptions = editor_subscriptions;
870 cx.notify();
871 }
872
873 cx.focus_self(window);
874 }
875
876 fn show_initialization_sequence_for_server(
877 &mut self,
878 id: &LogStoreEntryIdentifier<'_>,
879 window: &mut Window,
880 cx: &mut Context<Self>,
881 ) {
882 let rpc_log = self.log_store.update(cx, |log_store, _| {
883 log_store
884 .initialization_sequence_for_session(id)
885 .map(|state| log_contents(state.iter().cloned()))
886 });
887 if let Some(rpc_log) = rpc_log {
888 self.current_view = Some((id.session_id, View::InitializationSequence));
889 let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
890 let language = self.project.read(cx).languages().language_for_name("JSON");
891 editor
892 .read(cx)
893 .buffer()
894 .read(cx)
895 .as_singleton()
896 .expect("log buffer should be a singleton")
897 .update(cx, |_, cx| {
898 cx.spawn({
899 let buffer = cx.entity();
900 async move |_, cx| {
901 let language = language.await.ok();
902 buffer.update(cx, |buffer, cx| {
903 buffer.set_language(language, cx);
904 })
905 }
906 })
907 .detach_and_log_err(cx);
908 });
909
910 self.editor = editor;
911 self.editor_subscriptions = editor_subscriptions;
912 cx.notify();
913 }
914
915 cx.focus_self(window);
916 }
917}
918
919fn log_contents(lines: impl Iterator<Item = SharedString>) -> String {
920 lines.fold(String::new(), |mut acc, el| {
921 acc.push_str(&el);
922 acc.push('\n');
923 acc
924 })
925}
926
927#[derive(Clone, PartialEq)]
928struct DapMenuItem {
929 session_id: SessionId,
930 session_label: SharedString,
931 adapter_name: DebugAdapterName,
932 has_adapter_logs: bool,
933 selected_entry: View,
934}
935
936const ADAPTER_LOGS: &str = "Adapter Logs";
937const RPC_MESSAGES: &str = "RPC Messages";
938const INITIALIZATION_SEQUENCE: &str = "Initialization Sequence";
939
940impl Render for DapLogView {
941 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
942 self.editor.update(cx, |editor, cx| {
943 editor.render(window, cx).into_any_element()
944 })
945 }
946}
947
948actions!(
949 dev,
950 [
951 /// Opens the debug adapter protocol logs viewer.
952 OpenDebugAdapterLogs
953 ]
954);
955
956pub fn init(cx: &mut App) {
957 let log_store = cx.new(|cx| LogStore::new(cx));
958
959 cx.observe_new(move |workspace: &mut Workspace, window, cx| {
960 let Some(_window) = window else {
961 return;
962 };
963
964 let project = workspace.project();
965 if project.read(cx).is_local() {
966 log_store.update(cx, |store, cx| {
967 store.add_project(project, cx);
968 });
969 }
970
971 let log_store = log_store.clone();
972 workspace.register_action(move |workspace, _: &OpenDebugAdapterLogs, window, cx| {
973 let project = workspace.project().read(cx);
974 if project.is_local() {
975 workspace.add_item_to_active_pane(
976 Box::new(cx.new(|cx| {
977 DapLogView::new(workspace.project().clone(), log_store.clone(), window, cx)
978 })),
979 None,
980 true,
981 window,
982 cx,
983 );
984 }
985 });
986 })
987 .detach();
988}
989
990impl Item for DapLogView {
991 type Event = EditorEvent;
992
993 fn to_item_events(event: &Self::Event, f: impl FnMut(workspace::item::ItemEvent)) {
994 Editor::to_item_events(event, f)
995 }
996
997 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
998 "DAP Logs".into()
999 }
1000
1001 fn telemetry_event_text(&self) -> Option<&'static str> {
1002 None
1003 }
1004
1005 fn as_searchable(&self, handle: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
1006 Some(Box::new(handle.clone()))
1007 }
1008}
1009
1010impl SearchableItem for DapLogView {
1011 type Match = <Editor as SearchableItem>::Match;
1012
1013 fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1014 self.editor.update(cx, |e, cx| e.clear_matches(window, cx))
1015 }
1016
1017 fn update_matches(
1018 &mut self,
1019 matches: &[Self::Match],
1020 window: &mut Window,
1021 cx: &mut Context<Self>,
1022 ) {
1023 self.editor
1024 .update(cx, |e, cx| e.update_matches(matches, window, cx))
1025 }
1026
1027 fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {
1028 self.editor
1029 .update(cx, |e, cx| e.query_suggestion(window, cx))
1030 }
1031
1032 fn activate_match(
1033 &mut self,
1034 index: usize,
1035 matches: &[Self::Match],
1036 window: &mut Window,
1037 cx: &mut Context<Self>,
1038 ) {
1039 self.editor
1040 .update(cx, |e, cx| e.activate_match(index, matches, window, cx))
1041 }
1042
1043 fn select_matches(
1044 &mut self,
1045 matches: &[Self::Match],
1046 window: &mut Window,
1047 cx: &mut Context<Self>,
1048 ) {
1049 self.editor
1050 .update(cx, |e, cx| e.select_matches(matches, window, cx))
1051 }
1052
1053 fn find_matches(
1054 &mut self,
1055 query: Arc<project::search::SearchQuery>,
1056 window: &mut Window,
1057 cx: &mut Context<Self>,
1058 ) -> gpui::Task<Vec<Self::Match>> {
1059 self.editor
1060 .update(cx, |e, cx| e.find_matches(query, window, cx))
1061 }
1062
1063 fn replace(
1064 &mut self,
1065 _: &Self::Match,
1066 _: &SearchQuery,
1067 _window: &mut Window,
1068 _: &mut Context<Self>,
1069 ) {
1070 // Since DAP Log is read-only, it doesn't make sense to support replace operation.
1071 }
1072
1073 fn supported_options(&self) -> workspace::searchable::SearchOptions {
1074 workspace::searchable::SearchOptions {
1075 case: true,
1076 word: true,
1077 regex: true,
1078 find_in_results: true,
1079 // DAP log is read-only.
1080 replacement: false,
1081 selection: false,
1082 }
1083 }
1084 fn active_match_index(
1085 &mut self,
1086 direction: Direction,
1087 matches: &[Self::Match],
1088 window: &mut Window,
1089 cx: &mut Context<Self>,
1090 ) -> Option<usize> {
1091 self.editor.update(cx, |e, cx| {
1092 e.active_match_index(direction, matches, window, cx)
1093 })
1094 }
1095}
1096
1097impl Focusable for DapLogView {
1098 fn focus_handle(&self, _cx: &App) -> FocusHandle {
1099 self.focus_handle.clone()
1100 }
1101}
1102
1103enum Event {
1104 NewLogEntry {
1105 id: LogStoreEntryIdentifier<'static>,
1106 entry: SharedString,
1107 kind: LogKind,
1108 },
1109}
1110
1111impl EventEmitter<Event> for LogStore {}
1112impl EventEmitter<Event> for DapLogView {}
1113impl EventEmitter<EditorEvent> for DapLogView {}
1114impl EventEmitter<SearchEvent> for DapLogView {}
1115
1116#[cfg(any(test, feature = "test-support"))]
1117impl LogStore {
1118 pub fn has_projects(&self) -> bool {
1119 !self.projects.is_empty()
1120 }
1121
1122 pub fn contained_session_ids(&self, project: &WeakEntity<Project>) -> Vec<SessionId> {
1123 self.projects.get(project).map_or(vec![], |state| {
1124 state.debug_sessions.keys().copied().collect()
1125 })
1126 }
1127
1128 pub fn rpc_messages_for_session_id(
1129 &self,
1130 project: &WeakEntity<Project>,
1131 session_id: SessionId,
1132 ) -> Vec<SharedString> {
1133 self.projects.get(&project).map_or(vec![], |state| {
1134 state
1135 .debug_sessions
1136 .get(&session_id)
1137 .expect("This session should exist if a test is calling")
1138 .rpc_messages
1139 .messages
1140 .clone()
1141 .into()
1142 })
1143 }
1144}