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 {}