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