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