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 project
438 .debug_sessions
439 .retain(|_, session| !session.is_terminated);
440 });
441
442 cx.notify();
443 }
444
445 fn log_messages_for_session(
446 &mut self,
447 id: &LogStoreEntryIdentifier<'_>,
448 ) -> Option<&mut VecDeque<SharedString>> {
449 self.get_debug_adapter_state(id)
450 .map(|state| &mut state.log_messages)
451 }
452
453 fn rpc_messages_for_session(
454 &mut self,
455 id: &LogStoreEntryIdentifier<'_>,
456 ) -> Option<&mut VecDeque<SharedString>> {
457 self.get_debug_adapter_state(id)
458 .map(|state| &mut state.rpc_messages.messages)
459 }
460
461 fn initialization_sequence_for_session(
462 &mut self,
463 id: &LogStoreEntryIdentifier<'_>,
464 ) -> Option<&Vec<SharedString>> {
465 self.get_debug_adapter_state(&id)
466 .map(|state| &state.rpc_messages.initialization_sequence)
467 }
468}
469
470pub struct DapLogToolbarItemView {
471 log_view: Option<Entity<DapLogView>>,
472}
473
474impl DapLogToolbarItemView {
475 pub fn new() -> Self {
476 Self { log_view: None }
477 }
478}
479
480impl Render for DapLogToolbarItemView {
481 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
482 let Some(log_view) = self.log_view.clone() else {
483 return Empty.into_any_element();
484 };
485
486 let (menu_rows, current_session_id, project) = log_view.update(cx, |log_view, cx| {
487 (
488 log_view.menu_items(cx),
489 log_view.current_view.map(|(session_id, _)| session_id),
490 log_view.project.downgrade(),
491 )
492 });
493
494 let current_client = current_session_id
495 .and_then(|session_id| menu_rows.iter().find(|row| row.session_id == session_id));
496
497 let dap_menu: PopoverMenu<_> = PopoverMenu::new("DapLogView")
498 .anchor(gpui::Corner::TopLeft)
499 .trigger(Button::new(
500 "debug_client_menu_header",
501 current_client
502 .map(|sub_item| {
503 Cow::Owned(format!(
504 "{} ({}) - {}",
505 sub_item.adapter_name,
506 sub_item.session_id.0,
507 match sub_item.selected_entry {
508 LogKind::Adapter => ADAPTER_LOGS,
509 LogKind::Rpc => RPC_MESSAGES,
510 }
511 ))
512 })
513 .unwrap_or_else(|| "No adapter selected".into()),
514 ))
515 .menu(move |mut window, cx| {
516 let log_view = log_view.clone();
517 let menu_rows = menu_rows.clone();
518 let project = project.clone();
519 ContextMenu::build(&mut window, cx, move |mut menu, window, _cx| {
520 for row in menu_rows.into_iter() {
521 menu = menu.custom_row(move |_window, _cx| {
522 div()
523 .w_full()
524 .pl_2()
525 .child(
526 Label::new(format!(
527 "{}. {}",
528 row.session_id.0, row.adapter_name,
529 ))
530 .color(workspace::ui::Color::Muted),
531 )
532 .into_any_element()
533 });
534
535 if row.has_adapter_logs {
536 menu = menu.custom_entry(
537 move |_window, _cx| {
538 div()
539 .w_full()
540 .pl_4()
541 .child(Label::new(ADAPTER_LOGS))
542 .into_any_element()
543 },
544 window.handler_for(&log_view, {
545 let project = project.clone();
546 let id = LogStoreEntryIdentifier {
547 project: Cow::Owned(project),
548 session_id: row.session_id,
549 };
550 move |view, window, cx| {
551 view.show_log_messages_for_adapter(&id, window, cx);
552 }
553 }),
554 );
555 }
556
557 menu = menu
558 .custom_entry(
559 move |_window, _cx| {
560 div()
561 .w_full()
562 .pl_4()
563 .child(Label::new(RPC_MESSAGES))
564 .into_any_element()
565 },
566 window.handler_for(&log_view, {
567 let project = project.clone();
568 let id = LogStoreEntryIdentifier {
569 project: Cow::Owned(project),
570 session_id: row.session_id,
571 };
572 move |view, window, cx| {
573 view.show_rpc_trace_for_server(&id, window, cx);
574 }
575 }),
576 )
577 .custom_entry(
578 move |_window, _cx| {
579 div()
580 .w_full()
581 .pl_4()
582 .child(Label::new(INITIALIZATION_SEQUENCE))
583 .into_any_element()
584 },
585 window.handler_for(&log_view, {
586 let project = project.clone();
587 let id = LogStoreEntryIdentifier {
588 project: Cow::Owned(project),
589 session_id: row.session_id,
590 };
591 move |view, window, cx| {
592 view.show_initialization_sequence_for_server(
593 &id, window, cx,
594 );
595 }
596 }),
597 );
598 }
599
600 menu
601 })
602 .into()
603 });
604
605 h_flex()
606 .size_full()
607 .child(dap_menu)
608 .child(
609 div()
610 .child(
611 Button::new("clear_log_button", "Clear").on_click(cx.listener(
612 |this, _, window, cx| {
613 if let Some(log_view) = this.log_view.as_ref() {
614 log_view.update(cx, |log_view, cx| {
615 log_view.editor.update(cx, |editor, cx| {
616 editor.set_read_only(false);
617 editor.clear(window, cx);
618 editor.set_read_only(true);
619 });
620 })
621 }
622 },
623 )),
624 )
625 .ml_2(),
626 )
627 .into_any_element()
628 }
629}
630
631impl EventEmitter<ToolbarItemEvent> for DapLogToolbarItemView {}
632
633impl ToolbarItemView for DapLogToolbarItemView {
634 fn set_active_pane_item(
635 &mut self,
636 active_pane_item: Option<&dyn workspace::item::ItemHandle>,
637 _window: &mut Window,
638 cx: &mut Context<Self>,
639 ) -> workspace::ToolbarItemLocation {
640 if let Some(item) = active_pane_item {
641 if let Some(log_view) = item.downcast::<DapLogView>() {
642 self.log_view = Some(log_view.clone());
643 return workspace::ToolbarItemLocation::PrimaryLeft;
644 }
645 }
646 self.log_view = None;
647
648 cx.notify();
649
650 workspace::ToolbarItemLocation::Hidden
651 }
652}
653
654impl DapLogView {
655 pub fn new(
656 project: Entity<Project>,
657 log_store: Entity<LogStore>,
658 window: &mut Window,
659 cx: &mut Context<Self>,
660 ) -> Self {
661 let (editor, editor_subscriptions) = Self::editor_for_logs(String::new(), window, cx);
662
663 let focus_handle = cx.focus_handle();
664
665 let events_subscriptions = cx.subscribe(&log_store, |log_view, _, event, cx| match event {
666 Event::NewLogEntry { id, entry, kind } => {
667 if log_view.current_view == Some((id.session_id, *kind))
668 && log_view.project == *id.project
669 {
670 log_view.editor.update(cx, |editor, cx| {
671 editor.set_read_only(false);
672 let last_point = editor.buffer().read(cx).len(cx);
673 editor.edit(
674 vec![
675 (last_point..last_point, entry.trim()),
676 (last_point..last_point, "\n"),
677 ],
678 cx,
679 );
680 editor.set_read_only(true);
681 });
682 }
683 }
684 });
685 let weak_project = project.downgrade();
686 let state_info = log_store
687 .read(cx)
688 .projects
689 .get(&weak_project)
690 .and_then(|project| {
691 project
692 .debug_sessions
693 .values()
694 .next_back()
695 .map(|session| (session.id, session.has_adapter_logs))
696 });
697
698 let mut this = Self {
699 editor,
700 focus_handle,
701 project,
702 log_store,
703 editor_subscriptions,
704 current_view: None,
705 _subscriptions: vec![events_subscriptions],
706 };
707
708 if let Some((session_id, have_adapter_logs)) = state_info {
709 let id = LogStoreEntryIdentifier {
710 session_id,
711 project: Cow::Owned(weak_project),
712 };
713 if have_adapter_logs {
714 this.show_log_messages_for_adapter(&id, window, cx);
715 } else {
716 this.show_rpc_trace_for_server(&id, window, cx);
717 }
718 }
719
720 this
721 }
722
723 fn editor_for_logs(
724 log_contents: String,
725 window: &mut Window,
726 cx: &mut Context<Self>,
727 ) -> (Entity<Editor>, Vec<Subscription>) {
728 let editor = cx.new(|cx| {
729 let mut editor = Editor::multi_line(window, cx);
730 editor.set_text(log_contents, window, cx);
731 editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
732 editor.set_show_code_actions(false, cx);
733 editor.set_show_breakpoints(false, cx);
734 editor.set_show_git_diff_gutter(false, cx);
735 editor.set_show_runnables(false, cx);
736 editor.set_input_enabled(false);
737 editor.set_use_autoclose(false);
738 editor.set_read_only(true);
739 editor.set_show_edit_predictions(Some(false), window, cx);
740 editor
741 });
742 let editor_subscription = cx.subscribe(
743 &editor,
744 |_, _, event: &EditorEvent, cx: &mut Context<DapLogView>| cx.emit(event.clone()),
745 );
746 let search_subscription = cx.subscribe(
747 &editor,
748 |_, _, event: &SearchEvent, cx: &mut Context<DapLogView>| cx.emit(event.clone()),
749 );
750 (editor, vec![editor_subscription, search_subscription])
751 }
752
753 fn menu_items(&self, cx: &App) -> Vec<DapMenuItem> {
754 self.log_store
755 .read(cx)
756 .projects
757 .get(&self.project.downgrade())
758 .map_or_else(Vec::new, |state| {
759 state
760 .debug_sessions
761 .values()
762 .rev()
763 .map(|state| DapMenuItem {
764 session_id: state.id,
765 adapter_name: state.adapter_name.clone(),
766 has_adapter_logs: state.has_adapter_logs,
767 selected_entry: self
768 .current_view
769 .map_or(LogKind::Adapter, |(_, kind)| kind),
770 })
771 .collect::<Vec<_>>()
772 })
773 }
774
775 fn show_rpc_trace_for_server(
776 &mut self,
777 id: &LogStoreEntryIdentifier<'_>,
778 window: &mut Window,
779 cx: &mut Context<Self>,
780 ) {
781 let rpc_log = self.log_store.update(cx, |log_store, _| {
782 log_store
783 .rpc_messages_for_session(id)
784 .map(|state| log_contents(state.iter().cloned()))
785 });
786 if let Some(rpc_log) = rpc_log {
787 self.current_view = Some((id.session_id, LogKind::Rpc));
788 let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
789 let language = self.project.read(cx).languages().language_for_name("JSON");
790 editor
791 .read(cx)
792 .buffer()
793 .read(cx)
794 .as_singleton()
795 .expect("log buffer should be a singleton")
796 .update(cx, |_, cx| {
797 cx.spawn({
798 async move |buffer, cx| {
799 let language = language.await.ok();
800 buffer.update(cx, |buffer, cx| {
801 buffer.set_language(language, cx);
802 })
803 }
804 })
805 .detach_and_log_err(cx);
806 });
807
808 self.editor = editor;
809 self.editor_subscriptions = editor_subscriptions;
810 cx.notify();
811 }
812
813 cx.focus_self(window);
814 }
815
816 fn show_log_messages_for_adapter(
817 &mut self,
818 id: &LogStoreEntryIdentifier<'_>,
819 window: &mut Window,
820 cx: &mut Context<Self>,
821 ) {
822 let message_log = self.log_store.update(cx, |log_store, _| {
823 log_store
824 .log_messages_for_session(id)
825 .map(|state| log_contents(state.iter().cloned()))
826 });
827 if let Some(message_log) = message_log {
828 self.current_view = Some((id.session_id, LogKind::Adapter));
829 let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, window, cx);
830 editor
831 .read(cx)
832 .buffer()
833 .read(cx)
834 .as_singleton()
835 .expect("log buffer should be a singleton");
836
837 self.editor = editor;
838 self.editor_subscriptions = editor_subscriptions;
839 cx.notify();
840 }
841
842 cx.focus_self(window);
843 }
844
845 fn show_initialization_sequence_for_server(
846 &mut self,
847 id: &LogStoreEntryIdentifier<'_>,
848 window: &mut Window,
849 cx: &mut Context<Self>,
850 ) {
851 let rpc_log = self.log_store.update(cx, |log_store, _| {
852 log_store
853 .initialization_sequence_for_session(id)
854 .map(|state| log_contents(state.iter().cloned()))
855 });
856 if let Some(rpc_log) = rpc_log {
857 self.current_view = Some((id.session_id, LogKind::Rpc));
858 let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
859 let language = self.project.read(cx).languages().language_for_name("JSON");
860 editor
861 .read(cx)
862 .buffer()
863 .read(cx)
864 .as_singleton()
865 .expect("log buffer should be a singleton")
866 .update(cx, |_, cx| {
867 cx.spawn({
868 let buffer = cx.entity();
869 async move |_, cx| {
870 let language = language.await.ok();
871 buffer.update(cx, |buffer, cx| {
872 buffer.set_language(language, cx);
873 })
874 }
875 })
876 .detach_and_log_err(cx);
877 });
878
879 self.editor = editor;
880 self.editor_subscriptions = editor_subscriptions;
881 cx.notify();
882 }
883
884 cx.focus_self(window);
885 }
886}
887
888fn log_contents(lines: impl Iterator<Item = SharedString>) -> String {
889 lines.fold(String::new(), |mut acc, el| {
890 acc.push_str(&el);
891 acc.push('\n');
892 acc
893 })
894}
895
896#[derive(Clone, PartialEq)]
897pub(crate) struct DapMenuItem {
898 pub session_id: SessionId,
899 pub adapter_name: DebugAdapterName,
900 pub has_adapter_logs: bool,
901 pub selected_entry: LogKind,
902}
903
904const ADAPTER_LOGS: &str = "Adapter Logs";
905const RPC_MESSAGES: &str = "RPC Messages";
906const INITIALIZATION_SEQUENCE: &str = "Initialization Sequence";
907
908impl Render for DapLogView {
909 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
910 self.editor.update(cx, |editor, cx| {
911 editor.render(window, cx).into_any_element()
912 })
913 }
914}
915
916actions!(dev, [OpenDebugAdapterLogs]);
917
918pub fn init(cx: &mut App) {
919 let log_store = cx.new(|cx| LogStore::new(cx));
920
921 cx.observe_new(move |workspace: &mut Workspace, window, cx| {
922 let Some(_window) = window else {
923 return;
924 };
925
926 let project = workspace.project();
927 if project.read(cx).is_local() {
928 log_store.update(cx, |store, cx| {
929 store.add_project(project, cx);
930 });
931 }
932
933 let log_store = log_store.clone();
934 workspace.register_action(move |workspace, _: &OpenDebugAdapterLogs, window, cx| {
935 let project = workspace.project().read(cx);
936 if project.is_local() {
937 workspace.add_item_to_active_pane(
938 Box::new(cx.new(|cx| {
939 DapLogView::new(workspace.project().clone(), log_store.clone(), window, cx)
940 })),
941 None,
942 true,
943 window,
944 cx,
945 );
946 }
947 });
948 })
949 .detach();
950}
951
952impl Item for DapLogView {
953 type Event = EditorEvent;
954
955 fn to_item_events(event: &Self::Event, f: impl FnMut(workspace::item::ItemEvent)) {
956 Editor::to_item_events(event, f)
957 }
958
959 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
960 "DAP Logs".into()
961 }
962
963 fn telemetry_event_text(&self) -> Option<&'static str> {
964 None
965 }
966
967 fn as_searchable(&self, handle: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
968 Some(Box::new(handle.clone()))
969 }
970}
971
972impl SearchableItem for DapLogView {
973 type Match = <Editor as SearchableItem>::Match;
974
975 fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
976 self.editor.update(cx, |e, cx| e.clear_matches(window, cx))
977 }
978
979 fn update_matches(
980 &mut self,
981 matches: &[Self::Match],
982 window: &mut Window,
983 cx: &mut Context<Self>,
984 ) {
985 self.editor
986 .update(cx, |e, cx| e.update_matches(matches, window, cx))
987 }
988
989 fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {
990 self.editor
991 .update(cx, |e, cx| e.query_suggestion(window, cx))
992 }
993
994 fn activate_match(
995 &mut self,
996 index: usize,
997 matches: &[Self::Match],
998 window: &mut Window,
999 cx: &mut Context<Self>,
1000 ) {
1001 self.editor
1002 .update(cx, |e, cx| e.activate_match(index, matches, window, cx))
1003 }
1004
1005 fn select_matches(
1006 &mut self,
1007 matches: &[Self::Match],
1008 window: &mut Window,
1009 cx: &mut Context<Self>,
1010 ) {
1011 self.editor
1012 .update(cx, |e, cx| e.select_matches(matches, window, cx))
1013 }
1014
1015 fn find_matches(
1016 &mut self,
1017 query: Arc<project::search::SearchQuery>,
1018 window: &mut Window,
1019 cx: &mut Context<Self>,
1020 ) -> gpui::Task<Vec<Self::Match>> {
1021 self.editor
1022 .update(cx, |e, cx| e.find_matches(query, window, cx))
1023 }
1024
1025 fn replace(
1026 &mut self,
1027 _: &Self::Match,
1028 _: &SearchQuery,
1029 _window: &mut Window,
1030 _: &mut Context<Self>,
1031 ) {
1032 // Since DAP Log is read-only, it doesn't make sense to support replace operation.
1033 }
1034
1035 fn supported_options(&self) -> workspace::searchable::SearchOptions {
1036 workspace::searchable::SearchOptions {
1037 case: true,
1038 word: true,
1039 regex: true,
1040 find_in_results: true,
1041 // DAP log is read-only.
1042 replacement: false,
1043 selection: false,
1044 }
1045 }
1046 fn active_match_index(
1047 &mut self,
1048 direction: Direction,
1049 matches: &[Self::Match],
1050 window: &mut Window,
1051 cx: &mut Context<Self>,
1052 ) -> Option<usize> {
1053 self.editor.update(cx, |e, cx| {
1054 e.active_match_index(direction, matches, window, cx)
1055 })
1056 }
1057}
1058
1059impl Focusable for DapLogView {
1060 fn focus_handle(&self, _cx: &App) -> FocusHandle {
1061 self.focus_handle.clone()
1062 }
1063}
1064
1065enum Event {
1066 NewLogEntry {
1067 id: LogStoreEntryIdentifier<'static>,
1068 entry: SharedString,
1069 kind: LogKind,
1070 },
1071}
1072
1073impl EventEmitter<Event> for LogStore {}
1074impl EventEmitter<Event> for DapLogView {}
1075impl EventEmitter<EditorEvent> for DapLogView {}
1076impl EventEmitter<SearchEvent> for DapLogView {}
1077
1078#[cfg(any(test, feature = "test-support"))]
1079impl LogStore {
1080 pub fn has_projects(&self) -> bool {
1081 !self.projects.is_empty()
1082 }
1083
1084 pub fn contained_session_ids(&self, project: &WeakEntity<Project>) -> Vec<SessionId> {
1085 self.projects.get(project).map_or(vec![], |state| {
1086 state.debug_sessions.keys().copied().collect()
1087 })
1088 }
1089
1090 pub fn rpc_messages_for_session_id(
1091 &self,
1092 project: &WeakEntity<Project>,
1093 session_id: SessionId,
1094 ) -> Vec<SharedString> {
1095 self.projects.get(&project).map_or(vec![], |state| {
1096 state
1097 .debug_sessions
1098 .get(&session_id)
1099 .expect("This session should exist if a test is calling")
1100 .rpc_messages
1101 .messages
1102 .clone()
1103 .into()
1104 })
1105 }
1106}