Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:29:42.835Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

dap_log.rs

1162 lines · 38.3 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.