dap_log.rs

  1use dap::{
  2    client::SessionId,
  3    debugger_settings::DebuggerSettings,
  4    transport::{IoKind, LogKind},
  5};
  6use editor::{Editor, EditorEvent};
  7use futures::{
  8    StreamExt,
  9    channel::mpsc::{UnboundedSender, unbounded},
 10};
 11use gpui::{
 12    App, AppContext, Context, Empty, Entity, EventEmitter, FocusHandle, Focusable, IntoElement,
 13    ParentElement, Render, SharedString, Styled, Subscription, WeakEntity, Window, actions, div,
 14};
 15use project::{
 16    Project,
 17    debugger::{dap_store, session::Session},
 18    search::SearchQuery,
 19};
 20use settings::Settings as _;
 21use std::{
 22    borrow::Cow,
 23    collections::{HashMap, VecDeque},
 24    sync::Arc,
 25};
 26use util::maybe;
 27use workspace::{
 28    ToolbarItemEvent, ToolbarItemView, Workspace,
 29    item::Item,
 30    searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
 31    ui::{Button, Clickable, ContextMenu, Label, LabelCommon, PopoverMenu, h_flex},
 32};
 33
 34struct DapLogView {
 35    editor: Entity<Editor>,
 36    focus_handle: FocusHandle,
 37    log_store: Entity<LogStore>,
 38    editor_subscriptions: Vec<Subscription>,
 39    current_view: Option<(SessionId, LogKind)>,
 40    project: Entity<Project>,
 41    _subscriptions: Vec<Subscription>,
 42}
 43
 44pub struct LogStore {
 45    projects: HashMap<WeakEntity<Project>, ProjectState>,
 46    debug_clients: HashMap<SessionId, DebugAdapterState>,
 47    rpc_tx: UnboundedSender<(SessionId, IoKind, String)>,
 48    adapter_log_tx: UnboundedSender<(SessionId, IoKind, String)>,
 49}
 50
 51struct ProjectState {
 52    _subscriptions: [gpui::Subscription; 2],
 53}
 54
 55struct DebugAdapterState {
 56    log_messages: VecDeque<String>,
 57    rpc_messages: RpcMessages,
 58}
 59
 60struct RpcMessages {
 61    messages: VecDeque<String>,
 62    last_message_kind: Option<MessageKind>,
 63}
 64
 65impl RpcMessages {
 66    const MESSAGE_QUEUE_LIMIT: usize = 255;
 67
 68    fn new() -> Self {
 69        Self {
 70            last_message_kind: None,
 71            messages: VecDeque::with_capacity(Self::MESSAGE_QUEUE_LIMIT),
 72        }
 73    }
 74}
 75
 76const SEND: &str = "// Send";
 77const RECEIVE: &str = "// Receive";
 78
 79#[derive(Clone, Copy, PartialEq, Eq)]
 80enum MessageKind {
 81    Send,
 82    Receive,
 83}
 84
 85impl MessageKind {
 86    fn label(&self) -> &'static str {
 87        match self {
 88            Self::Send => SEND,
 89            Self::Receive => RECEIVE,
 90        }
 91    }
 92}
 93
 94impl DebugAdapterState {
 95    fn new() -> Self {
 96        Self {
 97            log_messages: VecDeque::new(),
 98            rpc_messages: RpcMessages::new(),
 99        }
100    }
101}
102
103impl LogStore {
104    pub fn new(cx: &Context<Self>) -> Self {
105        let (rpc_tx, mut rpc_rx) = unbounded::<(SessionId, IoKind, String)>();
106        cx.spawn(async move |this, cx| {
107            while let Some((client_id, io_kind, message)) = rpc_rx.next().await {
108                if let Some(this) = this.upgrade() {
109                    this.update(cx, |this, cx| {
110                        this.on_rpc_log(client_id, io_kind, &message, cx);
111                    })?;
112                }
113
114                smol::future::yield_now().await;
115            }
116            anyhow::Ok(())
117        })
118        .detach_and_log_err(cx);
119
120        let (adapter_log_tx, mut adapter_log_rx) = unbounded::<(SessionId, IoKind, String)>();
121        cx.spawn(async move |this, cx| {
122            while let Some((client_id, io_kind, message)) = adapter_log_rx.next().await {
123                if let Some(this) = this.upgrade() {
124                    this.update(cx, |this, cx| {
125                        this.on_adapter_log(client_id, io_kind, &message, cx);
126                    })?;
127                }
128
129                smol::future::yield_now().await;
130            }
131            anyhow::Ok(())
132        })
133        .detach_and_log_err(cx);
134        Self {
135            rpc_tx,
136            adapter_log_tx,
137            projects: HashMap::new(),
138            debug_clients: HashMap::new(),
139        }
140    }
141
142    fn on_rpc_log(
143        &mut self,
144        client_id: SessionId,
145        io_kind: IoKind,
146        message: &str,
147        cx: &mut Context<Self>,
148    ) {
149        self.add_debug_client_message(client_id, io_kind, message.to_string(), cx);
150    }
151
152    fn on_adapter_log(
153        &mut self,
154        client_id: SessionId,
155        io_kind: IoKind,
156        message: &str,
157        cx: &mut Context<Self>,
158    ) {
159        self.add_debug_client_log(client_id, io_kind, message.to_string(), cx);
160    }
161
162    pub fn add_project(&mut self, project: &Entity<Project>, cx: &mut Context<Self>) {
163        let weak_project = project.downgrade();
164        self.projects.insert(
165            project.downgrade(),
166            ProjectState {
167                _subscriptions: [
168                    cx.observe_release(project, move |this, _, _| {
169                        this.projects.remove(&weak_project);
170                    }),
171                    cx.subscribe(
172                        &project.read(cx).dap_store(),
173                        |this, dap_store, event, cx| match event {
174                            dap_store::DapStoreEvent::DebugClientStarted(session_id) => {
175                                let session = dap_store.read(cx).session_by_id(session_id);
176                                if let Some(session) = session {
177                                    this.add_debug_client(*session_id, session, cx);
178                                }
179                            }
180                            dap_store::DapStoreEvent::DebugClientShutdown(session_id) => {
181                                this.remove_debug_client(*session_id, cx);
182                            }
183
184                            _ => {}
185                        },
186                    ),
187                ],
188            },
189        );
190    }
191
192    fn get_debug_adapter_state(&mut self, id: SessionId) -> Option<&mut DebugAdapterState> {
193        self.debug_clients.get_mut(&id)
194    }
195
196    fn add_debug_client_message(
197        &mut self,
198        id: SessionId,
199        io_kind: IoKind,
200        message: String,
201        cx: &mut Context<Self>,
202    ) {
203        let Some(debug_client_state) = self.get_debug_adapter_state(id) else {
204            return;
205        };
206
207        let kind = match io_kind {
208            IoKind::StdOut | IoKind::StdErr => MessageKind::Receive,
209            IoKind::StdIn => MessageKind::Send,
210        };
211
212        let rpc_messages = &mut debug_client_state.rpc_messages;
213        if rpc_messages.last_message_kind != Some(kind) {
214            Self::add_debug_client_entry(
215                &mut rpc_messages.messages,
216                id,
217                kind.label().to_string(),
218                LogKind::Rpc,
219                cx,
220            );
221            rpc_messages.last_message_kind = Some(kind);
222        }
223        Self::add_debug_client_entry(&mut rpc_messages.messages, id, message, LogKind::Rpc, cx);
224
225        cx.notify();
226    }
227
228    fn add_debug_client_log(
229        &mut self,
230        id: SessionId,
231        io_kind: IoKind,
232        message: String,
233        cx: &mut Context<Self>,
234    ) {
235        let Some(debug_client_state) = self.get_debug_adapter_state(id) else {
236            return;
237        };
238
239        let message = match io_kind {
240            IoKind::StdErr => {
241                let mut message = message.clone();
242                message.insert_str(0, "stderr: ");
243                message
244            }
245            _ => message,
246        };
247
248        Self::add_debug_client_entry(
249            &mut debug_client_state.log_messages,
250            id,
251            message,
252            LogKind::Adapter,
253            cx,
254        );
255        cx.notify();
256    }
257
258    fn add_debug_client_entry(
259        log_lines: &mut VecDeque<String>,
260        id: SessionId,
261        message: String,
262        kind: LogKind,
263        cx: &mut Context<Self>,
264    ) {
265        while log_lines.len() >= RpcMessages::MESSAGE_QUEUE_LIMIT {
266            log_lines.pop_front();
267        }
268
269        let format_messages = DebuggerSettings::get_global(cx).format_dap_log_messages;
270
271        let entry = if format_messages {
272            maybe!({
273                serde_json::to_string_pretty::<serde_json::Value>(
274                    &serde_json::from_str(&message).ok()?,
275                )
276                .ok()
277            })
278            .unwrap_or(message)
279        } else {
280            message
281        };
282        log_lines.push_back(entry.clone());
283
284        cx.emit(Event::NewLogEntry { id, entry, kind });
285    }
286
287    fn add_debug_client(
288        &mut self,
289        client_id: SessionId,
290        client: Entity<Session>,
291        cx: &App,
292    ) -> Option<&mut DebugAdapterState> {
293        let client_state = self
294            .debug_clients
295            .entry(client_id)
296            .or_insert_with(DebugAdapterState::new);
297
298        let io_tx = self.rpc_tx.clone();
299
300        let client = client.read(cx).adapter_client()?;
301        client.add_log_handler(
302            move |io_kind, message| {
303                io_tx
304                    .unbounded_send((client_id, io_kind, message.to_string()))
305                    .ok();
306            },
307            LogKind::Rpc,
308        );
309
310        let log_io_tx = self.adapter_log_tx.clone();
311        client.add_log_handler(
312            move |io_kind, message| {
313                log_io_tx
314                    .unbounded_send((client_id, io_kind, message.to_string()))
315                    .ok();
316            },
317            LogKind::Adapter,
318        );
319
320        Some(client_state)
321    }
322
323    fn remove_debug_client(&mut self, client_id: SessionId, cx: &mut Context<Self>) {
324        self.debug_clients.remove(&client_id);
325        cx.notify();
326    }
327
328    fn log_messages_for_client(&mut self, client_id: SessionId) -> Option<&mut VecDeque<String>> {
329        Some(&mut self.debug_clients.get_mut(&client_id)?.log_messages)
330    }
331
332    fn rpc_messages_for_client(&mut self, client_id: SessionId) -> Option<&mut VecDeque<String>> {
333        Some(
334            &mut self
335                .debug_clients
336                .get_mut(&client_id)?
337                .rpc_messages
338                .messages,
339        )
340    }
341}
342
343pub struct DapLogToolbarItemView {
344    log_view: Option<Entity<DapLogView>>,
345}
346
347impl DapLogToolbarItemView {
348    pub fn new() -> Self {
349        Self { log_view: None }
350    }
351}
352
353impl Render for DapLogToolbarItemView {
354    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
355        let Some(log_view) = self.log_view.clone() else {
356            return Empty.into_any_element();
357        };
358
359        let (menu_rows, current_client_id) = log_view.update(cx, |log_view, cx| {
360            (
361                log_view.menu_items(cx).unwrap_or_default(),
362                log_view.current_view.map(|(client_id, _)| client_id),
363            )
364        });
365
366        let current_client = current_client_id.and_then(|current_client_id| {
367            menu_rows
368                .iter()
369                .find(|row| row.client_id == current_client_id)
370        });
371
372        let dap_menu: PopoverMenu<_> = PopoverMenu::new("DapLogView")
373            .anchor(gpui::Corner::TopLeft)
374            .trigger(Button::new(
375                "debug_client_menu_header",
376                current_client
377                    .map(|sub_item| {
378                        Cow::Owned(format!(
379                            "{} ({}) - {}",
380                            sub_item.client_name,
381                            sub_item.client_id.0,
382                            match sub_item.selected_entry {
383                                LogKind::Adapter => ADAPTER_LOGS,
384                                LogKind::Rpc => RPC_MESSAGES,
385                            }
386                        ))
387                    })
388                    .unwrap_or_else(|| "No adapter selected".into()),
389            ))
390            .menu(move |mut window, cx| {
391                let log_view = log_view.clone();
392                let menu_rows = menu_rows.clone();
393                ContextMenu::build(&mut window, cx, move |mut menu, window, _cx| {
394                    for row in menu_rows.into_iter() {
395                        menu = menu.custom_row(move |_window, _cx| {
396                            div()
397                                .w_full()
398                                .pl_2()
399                                .child(
400                                    Label::new(
401                                        format!("{}. {}", row.client_id.0, row.client_name,),
402                                    )
403                                    .color(workspace::ui::Color::Muted),
404                                )
405                                .into_any_element()
406                        });
407
408                        if row.has_adapter_logs {
409                            menu = menu.custom_entry(
410                                move |_window, _cx| {
411                                    div()
412                                        .w_full()
413                                        .pl_4()
414                                        .child(Label::new(ADAPTER_LOGS))
415                                        .into_any_element()
416                                },
417                                window.handler_for(&log_view, move |view, window, cx| {
418                                    view.show_log_messages_for_adapter(row.client_id, window, cx);
419                                }),
420                            );
421                        }
422
423                        menu = menu.custom_entry(
424                            move |_window, _cx| {
425                                div()
426                                    .w_full()
427                                    .pl_4()
428                                    .child(Label::new(RPC_MESSAGES))
429                                    .into_any_element()
430                            },
431                            window.handler_for(&log_view, move |view, window, cx| {
432                                view.show_rpc_trace_for_server(row.client_id, window, cx);
433                            }),
434                        );
435                    }
436
437                    menu
438                })
439                .into()
440            });
441
442        h_flex()
443            .size_full()
444            .child(dap_menu)
445            .child(
446                div()
447                    .child(
448                        Button::new("clear_log_button", "Clear").on_click(cx.listener(
449                            |this, _, window, cx| {
450                                if let Some(log_view) = this.log_view.as_ref() {
451                                    log_view.update(cx, |log_view, cx| {
452                                        log_view.editor.update(cx, |editor, cx| {
453                                            editor.set_read_only(false);
454                                            editor.clear(window, cx);
455                                            editor.set_read_only(true);
456                                        });
457                                    })
458                                }
459                            },
460                        )),
461                    )
462                    .ml_2(),
463            )
464            .into_any_element()
465    }
466}
467
468impl EventEmitter<ToolbarItemEvent> for DapLogToolbarItemView {}
469
470impl ToolbarItemView for DapLogToolbarItemView {
471    fn set_active_pane_item(
472        &mut self,
473        active_pane_item: Option<&dyn workspace::item::ItemHandle>,
474        _window: &mut Window,
475        cx: &mut Context<Self>,
476    ) -> workspace::ToolbarItemLocation {
477        if let Some(item) = active_pane_item {
478            if let Some(log_view) = item.downcast::<DapLogView>() {
479                self.log_view = Some(log_view.clone());
480                return workspace::ToolbarItemLocation::PrimaryLeft;
481            }
482        }
483        self.log_view = None;
484
485        cx.notify();
486
487        workspace::ToolbarItemLocation::Hidden
488    }
489}
490
491impl DapLogView {
492    pub fn new(
493        project: Entity<Project>,
494        log_store: Entity<LogStore>,
495        window: &mut Window,
496        cx: &mut Context<Self>,
497    ) -> Self {
498        let (editor, editor_subscriptions) = Self::editor_for_logs(String::new(), window, cx);
499
500        let focus_handle = cx.focus_handle();
501
502        let events_subscriptions = cx.subscribe(&log_store, |log_view, _, event, cx| match event {
503            Event::NewLogEntry { id, entry, kind } => {
504                if log_view.current_view == Some((*id, *kind)) {
505                    log_view.editor.update(cx, |editor, cx| {
506                        editor.set_read_only(false);
507                        let last_point = editor.buffer().read(cx).len(cx);
508                        editor.edit(
509                            vec![
510                                (last_point..last_point, entry.trim()),
511                                (last_point..last_point, "\n"),
512                            ],
513                            cx,
514                        );
515                        editor.set_read_only(true);
516                    });
517                }
518            }
519        });
520
521        Self {
522            editor,
523            focus_handle,
524            project,
525            log_store,
526            editor_subscriptions,
527            current_view: None,
528            _subscriptions: vec![events_subscriptions],
529        }
530    }
531
532    fn editor_for_logs(
533        log_contents: String,
534        window: &mut Window,
535        cx: &mut Context<Self>,
536    ) -> (Entity<Editor>, Vec<Subscription>) {
537        let editor = cx.new(|cx| {
538            let mut editor = Editor::multi_line(window, cx);
539            editor.set_text(log_contents, window, cx);
540            editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
541            editor.set_show_code_actions(false, cx);
542            editor.set_show_breakpoints(false, cx);
543            editor.set_show_git_diff_gutter(false, cx);
544            editor.set_show_runnables(false, cx);
545            editor.set_input_enabled(false);
546            editor.set_use_autoclose(false);
547            editor.set_read_only(true);
548            editor.set_show_edit_predictions(Some(false), window, cx);
549            editor
550        });
551        let editor_subscription = cx.subscribe(
552            &editor,
553            |_, _, event: &EditorEvent, cx: &mut Context<DapLogView>| cx.emit(event.clone()),
554        );
555        let search_subscription = cx.subscribe(
556            &editor,
557            |_, _, event: &SearchEvent, cx: &mut Context<DapLogView>| cx.emit(event.clone()),
558        );
559        (editor, vec![editor_subscription, search_subscription])
560    }
561
562    fn menu_items(&self, cx: &App) -> Option<Vec<DapMenuItem>> {
563        let mut menu_items = self
564            .project
565            .read(cx)
566            .dap_store()
567            .read(cx)
568            .sessions()
569            .filter_map(|session| {
570                let session = session.read(cx);
571                session.adapter();
572                let client = session.adapter_client()?;
573                Some(DapMenuItem {
574                    client_id: client.id(),
575                    client_name: session.adapter().to_string(),
576                    has_adapter_logs: client.has_adapter_logs(),
577                    selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind),
578                })
579            })
580            .collect::<Vec<_>>();
581        menu_items.sort_by_key(|item| item.client_id.0);
582        Some(menu_items)
583    }
584
585    fn show_rpc_trace_for_server(
586        &mut self,
587        client_id: SessionId,
588        window: &mut Window,
589        cx: &mut Context<Self>,
590    ) {
591        let rpc_log = self.log_store.update(cx, |log_store, _| {
592            log_store
593                .rpc_messages_for_client(client_id)
594                .map(|state| log_contents(&state))
595        });
596        if let Some(rpc_log) = rpc_log {
597            self.current_view = Some((client_id, LogKind::Rpc));
598            let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
599            let language = self.project.read(cx).languages().language_for_name("JSON");
600            editor
601                .read(cx)
602                .buffer()
603                .read(cx)
604                .as_singleton()
605                .expect("log buffer should be a singleton")
606                .update(cx, |_, cx| {
607                    cx.spawn({
608                        let buffer = cx.entity();
609                        async move |_, cx| {
610                            let language = language.await.ok();
611                            buffer.update(cx, |buffer, cx| {
612                                buffer.set_language(language, cx);
613                            })
614                        }
615                    })
616                    .detach_and_log_err(cx);
617                });
618
619            self.editor = editor;
620            self.editor_subscriptions = editor_subscriptions;
621            cx.notify();
622        }
623
624        cx.focus_self(window);
625    }
626
627    fn show_log_messages_for_adapter(
628        &mut self,
629        client_id: SessionId,
630        window: &mut Window,
631        cx: &mut Context<Self>,
632    ) {
633        let message_log = self.log_store.update(cx, |log_store, _| {
634            log_store
635                .log_messages_for_client(client_id)
636                .map(|state| log_contents(&state))
637        });
638        if let Some(message_log) = message_log {
639            self.current_view = Some((client_id, LogKind::Adapter));
640            let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, window, cx);
641            editor
642                .read(cx)
643                .buffer()
644                .read(cx)
645                .as_singleton()
646                .expect("log buffer should be a singleton");
647
648            self.editor = editor;
649            self.editor_subscriptions = editor_subscriptions;
650            cx.notify();
651        }
652
653        cx.focus_self(window);
654    }
655}
656
657fn log_contents(lines: &VecDeque<String>) -> String {
658    let (a, b) = lines.as_slices();
659    let a = a.iter().map(move |v| v.as_ref());
660    let b = b.iter().map(move |v| v.as_ref());
661    a.chain(b).fold(String::new(), |mut acc, el| {
662        acc.push_str(el);
663        acc.push('\n');
664        acc
665    })
666}
667
668#[derive(Clone, PartialEq)]
669pub(crate) struct DapMenuItem {
670    pub client_id: SessionId,
671    pub client_name: String,
672    pub has_adapter_logs: bool,
673    pub selected_entry: LogKind,
674}
675
676const ADAPTER_LOGS: &str = "Adapter Logs";
677const RPC_MESSAGES: &str = "RPC Messages";
678
679impl Render for DapLogView {
680    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
681        self.editor.update(cx, |editor, cx| {
682            editor.render(window, cx).into_any_element()
683        })
684    }
685}
686
687actions!(dev, [OpenDebugAdapterLogs]);
688
689pub fn init(cx: &mut App) {
690    let log_store = cx.new(|cx| LogStore::new(cx));
691
692    cx.observe_new(move |workspace: &mut Workspace, window, cx| {
693        let Some(_window) = window else {
694            return;
695        };
696
697        let project = workspace.project();
698        if project.read(cx).is_local() {
699            log_store.update(cx, |store, cx| {
700                store.add_project(project, cx);
701            });
702        }
703
704        let log_store = log_store.clone();
705        workspace.register_action(move |workspace, _: &OpenDebugAdapterLogs, window, cx| {
706            let project = workspace.project().read(cx);
707            if project.is_local() {
708                workspace.add_item_to_active_pane(
709                    Box::new(cx.new(|cx| {
710                        DapLogView::new(workspace.project().clone(), log_store.clone(), window, cx)
711                    })),
712                    None,
713                    true,
714                    window,
715                    cx,
716                );
717            }
718        });
719    })
720    .detach();
721}
722
723impl Item for DapLogView {
724    type Event = EditorEvent;
725
726    fn to_item_events(event: &Self::Event, f: impl FnMut(workspace::item::ItemEvent)) {
727        Editor::to_item_events(event, f)
728    }
729
730    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
731        "DAP Logs".into()
732    }
733
734    fn telemetry_event_text(&self) -> Option<&'static str> {
735        None
736    }
737
738    fn as_searchable(&self, handle: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
739        Some(Box::new(handle.clone()))
740    }
741}
742
743impl SearchableItem for DapLogView {
744    type Match = <Editor as SearchableItem>::Match;
745
746    fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
747        self.editor.update(cx, |e, cx| e.clear_matches(window, cx))
748    }
749
750    fn update_matches(
751        &mut self,
752        matches: &[Self::Match],
753        window: &mut Window,
754        cx: &mut Context<Self>,
755    ) {
756        self.editor
757            .update(cx, |e, cx| e.update_matches(matches, window, cx))
758    }
759
760    fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {
761        self.editor
762            .update(cx, |e, cx| e.query_suggestion(window, cx))
763    }
764
765    fn activate_match(
766        &mut self,
767        index: usize,
768        matches: &[Self::Match],
769        window: &mut Window,
770        cx: &mut Context<Self>,
771    ) {
772        self.editor
773            .update(cx, |e, cx| e.activate_match(index, matches, window, cx))
774    }
775
776    fn select_matches(
777        &mut self,
778        matches: &[Self::Match],
779        window: &mut Window,
780        cx: &mut Context<Self>,
781    ) {
782        self.editor
783            .update(cx, |e, cx| e.select_matches(matches, window, cx))
784    }
785
786    fn find_matches(
787        &mut self,
788        query: Arc<project::search::SearchQuery>,
789        window: &mut Window,
790        cx: &mut Context<Self>,
791    ) -> gpui::Task<Vec<Self::Match>> {
792        self.editor
793            .update(cx, |e, cx| e.find_matches(query, window, cx))
794    }
795
796    fn replace(
797        &mut self,
798        _: &Self::Match,
799        _: &SearchQuery,
800        _window: &mut Window,
801        _: &mut Context<Self>,
802    ) {
803        // Since DAP Log is read-only, it doesn't make sense to support replace operation.
804    }
805
806    fn supported_options(&self) -> workspace::searchable::SearchOptions {
807        workspace::searchable::SearchOptions {
808            case: true,
809            word: true,
810            regex: true,
811            find_in_results: true,
812            // DAP log is read-only.
813            replacement: false,
814            selection: false,
815        }
816    }
817    fn active_match_index(
818        &mut self,
819        direction: Direction,
820        matches: &[Self::Match],
821        window: &mut Window,
822        cx: &mut Context<Self>,
823    ) -> Option<usize> {
824        self.editor.update(cx, |e, cx| {
825            e.active_match_index(direction, matches, window, cx)
826        })
827    }
828}
829
830impl Focusable for DapLogView {
831    fn focus_handle(&self, _cx: &App) -> FocusHandle {
832        self.focus_handle.clone()
833    }
834}
835
836pub enum Event {
837    NewLogEntry {
838        id: SessionId,
839        entry: String,
840        kind: LogKind,
841    },
842}
843
844impl EventEmitter<Event> for LogStore {}
845impl EventEmitter<Event> for DapLogView {}
846impl EventEmitter<EditorEvent> for DapLogView {}
847impl EventEmitter<SearchEvent> for DapLogView {}
848
849#[cfg(any(test, feature = "test-support"))]
850impl LogStore {
851    pub fn contained_session_ids(&self) -> Vec<SessionId> {
852        self.debug_clients.keys().cloned().collect()
853    }
854
855    pub fn rpc_messages_for_session_id(&self, session_id: SessionId) -> Vec<String> {
856        self.debug_clients
857            .get(&session_id)
858            .expect("This session should exist if a test is calling")
859            .rpc_messages
860            .messages
861            .clone()
862            .into()
863    }
864
865    pub fn log_messages_for_session_id(&self, session_id: SessionId) -> Vec<String> {
866        self.debug_clients
867            .get(&session_id)
868            .expect("This session should exist if a test is calling")
869            .log_messages
870            .clone()
871            .into()
872    }
873}