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