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
 44struct 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    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(|client| {
570                let client = client.read(cx).adapter_client()?;
571                Some(DapMenuItem {
572                    client_id: client.id(),
573                    client_name: client.name().0.as_ref().into(),
574                    has_adapter_logs: client.has_adapter_logs(),
575                    selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind),
576                })
577            })
578            .collect::<Vec<_>>();
579        menu_items.sort_by_key(|item| item.client_id.0);
580        Some(menu_items)
581    }
582
583    fn show_rpc_trace_for_server(
584        &mut self,
585        client_id: SessionId,
586        window: &mut Window,
587        cx: &mut Context<Self>,
588    ) {
589        let rpc_log = self.log_store.update(cx, |log_store, _| {
590            log_store
591                .rpc_messages_for_client(client_id)
592                .map(|state| log_contents(&state))
593        });
594        if let Some(rpc_log) = rpc_log {
595            self.current_view = Some((client_id, LogKind::Rpc));
596            let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
597            let language = self.project.read(cx).languages().language_for_name("JSON");
598            editor
599                .read(cx)
600                .buffer()
601                .read(cx)
602                .as_singleton()
603                .expect("log buffer should be a singleton")
604                .update(cx, |_, cx| {
605                    cx.spawn({
606                        let buffer = cx.entity();
607                        async move |_, cx| {
608                            let language = language.await.ok();
609                            buffer.update(cx, |buffer, cx| {
610                                buffer.set_language(language, cx);
611                            })
612                        }
613                    })
614                    .detach_and_log_err(cx);
615                });
616
617            self.editor = editor;
618            self.editor_subscriptions = editor_subscriptions;
619            cx.notify();
620        }
621
622        cx.focus_self(window);
623    }
624
625    fn show_log_messages_for_adapter(
626        &mut self,
627        client_id: SessionId,
628        window: &mut Window,
629        cx: &mut Context<Self>,
630    ) {
631        let message_log = self.log_store.update(cx, |log_store, _| {
632            log_store
633                .log_messages_for_client(client_id)
634                .map(|state| log_contents(&state))
635        });
636        if let Some(message_log) = message_log {
637            self.current_view = Some((client_id, LogKind::Adapter));
638            let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, window, cx);
639            editor
640                .read(cx)
641                .buffer()
642                .read(cx)
643                .as_singleton()
644                .expect("log buffer should be a singleton");
645
646            self.editor = editor;
647            self.editor_subscriptions = editor_subscriptions;
648            cx.notify();
649        }
650
651        cx.focus_self(window);
652    }
653}
654
655fn log_contents(lines: &VecDeque<String>) -> String {
656    let (a, b) = lines.as_slices();
657    let a = a.iter().map(move |v| v.as_ref());
658    let b = b.iter().map(move |v| v.as_ref());
659    a.chain(b).fold(String::new(), |mut acc, el| {
660        acc.push_str(el);
661        acc.push('\n');
662        acc
663    })
664}
665
666#[derive(Clone, PartialEq)]
667pub(crate) struct DapMenuItem {
668    pub client_id: SessionId,
669    pub client_name: String,
670    pub has_adapter_logs: bool,
671    pub selected_entry: LogKind,
672}
673
674const ADAPTER_LOGS: &str = "Adapter Logs";
675const RPC_MESSAGES: &str = "RPC Messages";
676
677impl Render for DapLogView {
678    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
679        self.editor.update(cx, |editor, cx| {
680            editor.render(window, cx).into_any_element()
681        })
682    }
683}
684
685actions!(debug, [OpenDebuggerAdapterLogs]);
686
687pub fn init(cx: &mut App) {
688    let log_store = cx.new(|cx| LogStore::new(cx));
689
690    cx.observe_new(move |workspace: &mut Workspace, window, cx| {
691        let Some(_window) = window else {
692            return;
693        };
694
695        let project = workspace.project();
696        if project.read(cx).is_local() {
697            log_store.update(cx, |store, cx| {
698                store.add_project(project, cx);
699            });
700        }
701
702        let log_store = log_store.clone();
703        workspace.register_action(move |workspace, _: &OpenDebuggerAdapterLogs, window, cx| {
704            let project = workspace.project().read(cx);
705            if project.is_local() {
706                workspace.add_item_to_active_pane(
707                    Box::new(cx.new(|cx| {
708                        DapLogView::new(workspace.project().clone(), log_store.clone(), window, cx)
709                    })),
710                    None,
711                    true,
712                    window,
713                    cx,
714                );
715            }
716        });
717    })
718    .detach();
719}
720
721impl Item for DapLogView {
722    type Event = EditorEvent;
723
724    fn to_item_events(event: &Self::Event, f: impl FnMut(workspace::item::ItemEvent)) {
725        Editor::to_item_events(event, f)
726    }
727
728    fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
729        Some("DAP Logs".into())
730    }
731
732    fn telemetry_event_text(&self) -> Option<&'static str> {
733        None
734    }
735
736    fn as_searchable(&self, handle: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
737        Some(Box::new(handle.clone()))
738    }
739}
740
741impl SearchableItem for DapLogView {
742    type Match = <Editor as SearchableItem>::Match;
743
744    fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
745        self.editor.update(cx, |e, cx| e.clear_matches(window, cx))
746    }
747
748    fn update_matches(
749        &mut self,
750        matches: &[Self::Match],
751        window: &mut Window,
752        cx: &mut Context<Self>,
753    ) {
754        self.editor
755            .update(cx, |e, cx| e.update_matches(matches, window, cx))
756    }
757
758    fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {
759        self.editor
760            .update(cx, |e, cx| e.query_suggestion(window, cx))
761    }
762
763    fn activate_match(
764        &mut self,
765        index: usize,
766        matches: &[Self::Match],
767        window: &mut Window,
768        cx: &mut Context<Self>,
769    ) {
770        self.editor
771            .update(cx, |e, cx| e.activate_match(index, matches, window, cx))
772    }
773
774    fn select_matches(
775        &mut self,
776        matches: &[Self::Match],
777        window: &mut Window,
778        cx: &mut Context<Self>,
779    ) {
780        self.editor
781            .update(cx, |e, cx| e.select_matches(matches, window, cx))
782    }
783
784    fn find_matches(
785        &mut self,
786        query: Arc<project::search::SearchQuery>,
787        window: &mut Window,
788        cx: &mut Context<Self>,
789    ) -> gpui::Task<Vec<Self::Match>> {
790        self.editor
791            .update(cx, |e, cx| e.find_matches(query, window, cx))
792    }
793
794    fn replace(
795        &mut self,
796        _: &Self::Match,
797        _: &SearchQuery,
798        _window: &mut Window,
799        _: &mut Context<Self>,
800    ) {
801        // Since DAP Log is read-only, it doesn't make sense to support replace operation.
802    }
803
804    fn supported_options(&self) -> workspace::searchable::SearchOptions {
805        workspace::searchable::SearchOptions {
806            case: true,
807            word: true,
808            regex: true,
809            find_in_results: true,
810            // DAP log is read-only.
811            replacement: false,
812            selection: false,
813        }
814    }
815    fn active_match_index(
816        &mut self,
817        direction: Direction,
818        matches: &[Self::Match],
819        window: &mut Window,
820        cx: &mut Context<Self>,
821    ) -> Option<usize> {
822        self.editor.update(cx, |e, cx| {
823            e.active_match_index(direction, matches, window, cx)
824        })
825    }
826}
827
828impl Focusable for DapLogView {
829    fn focus_handle(&self, _cx: &App) -> FocusHandle {
830        self.focus_handle.clone()
831    }
832}
833
834pub enum Event {
835    NewLogEntry {
836        id: SessionId,
837        entry: String,
838        kind: LogKind,
839    },
840}
841
842impl EventEmitter<Event> for LogStore {}
843impl EventEmitter<Event> for DapLogView {}
844impl EventEmitter<EditorEvent> for DapLogView {}
845impl EventEmitter<SearchEvent> for DapLogView {}