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}