dap_log.rs

   1use dap::{
   2    adapters::DebugAdapterName,
   3    client::SessionId,
   4    debugger_settings::DebuggerSettings,
   5    transport::{IoKind, LogKind},
   6};
   7use editor::{Editor, EditorEvent};
   8use futures::{
   9    StreamExt,
  10    channel::mpsc::{UnboundedSender, unbounded},
  11};
  12use gpui::{
  13    App, AppContext, Context, Empty, Entity, EventEmitter, FocusHandle, Focusable, IntoElement,
  14    ParentElement, Render, SharedString, Styled, Subscription, WeakEntity, Window, actions, div,
  15};
  16use project::{
  17    Project,
  18    debugger::{dap_store, session::Session},
  19    search::SearchQuery,
  20};
  21use settings::Settings as _;
  22use std::{
  23    borrow::Cow,
  24    collections::{BTreeMap, HashMap, VecDeque},
  25    sync::Arc,
  26};
  27use util::maybe;
  28use workspace::{
  29    ToolbarItemEvent, ToolbarItemView, Workspace,
  30    item::Item,
  31    searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
  32    ui::{Button, Clickable, ContextMenu, Label, LabelCommon, PopoverMenu, h_flex},
  33};
  34
  35struct DapLogView {
  36    editor: Entity<Editor>,
  37    focus_handle: FocusHandle,
  38    log_store: Entity<LogStore>,
  39    editor_subscriptions: Vec<Subscription>,
  40    current_view: Option<(SessionId, LogKind)>,
  41    project: Entity<Project>,
  42    _subscriptions: Vec<Subscription>,
  43}
  44
  45struct LogStoreEntryIdentifier<'a> {
  46    session_id: SessionId,
  47    project: Cow<'a, WeakEntity<Project>>,
  48}
  49impl LogStoreEntryIdentifier<'_> {
  50    fn to_owned(&self) -> LogStoreEntryIdentifier<'static> {
  51        LogStoreEntryIdentifier {
  52            session_id: self.session_id,
  53            project: Cow::Owned(self.project.as_ref().clone()),
  54        }
  55    }
  56}
  57
  58struct LogStoreMessage {
  59    id: LogStoreEntryIdentifier<'static>,
  60    kind: IoKind,
  61    command: Option<SharedString>,
  62    message: SharedString,
  63}
  64
  65pub struct LogStore {
  66    projects: HashMap<WeakEntity<Project>, ProjectState>,
  67    rpc_tx: UnboundedSender<LogStoreMessage>,
  68    adapter_log_tx: UnboundedSender<LogStoreMessage>,
  69}
  70
  71struct ProjectState {
  72    debug_sessions: BTreeMap<SessionId, DebugAdapterState>,
  73    _subscriptions: [gpui::Subscription; 2],
  74}
  75
  76struct DebugAdapterState {
  77    id: SessionId,
  78    log_messages: VecDeque<SharedString>,
  79    rpc_messages: RpcMessages,
  80    adapter_name: DebugAdapterName,
  81    has_adapter_logs: bool,
  82    is_terminated: bool,
  83}
  84
  85struct RpcMessages {
  86    messages: VecDeque<SharedString>,
  87    last_message_kind: Option<MessageKind>,
  88    initialization_sequence: Vec<SharedString>,
  89    last_init_message_kind: Option<MessageKind>,
  90}
  91
  92impl RpcMessages {
  93    const MESSAGE_QUEUE_LIMIT: usize = 255;
  94
  95    fn new() -> Self {
  96        Self {
  97            last_message_kind: None,
  98            last_init_message_kind: None,
  99            messages: VecDeque::with_capacity(Self::MESSAGE_QUEUE_LIMIT),
 100            initialization_sequence: Vec::new(),
 101        }
 102    }
 103}
 104
 105const SEND: &str = "// Send";
 106const RECEIVE: &str = "// Receive";
 107
 108#[derive(Clone, Copy, PartialEq, Eq)]
 109enum MessageKind {
 110    Send,
 111    Receive,
 112}
 113
 114impl MessageKind {
 115    fn label(&self) -> &'static str {
 116        match self {
 117            Self::Send => SEND,
 118            Self::Receive => RECEIVE,
 119        }
 120    }
 121}
 122
 123impl DebugAdapterState {
 124    fn new(id: SessionId, adapter_name: DebugAdapterName, has_adapter_logs: bool) -> Self {
 125        Self {
 126            id,
 127            log_messages: VecDeque::new(),
 128            rpc_messages: RpcMessages::new(),
 129            adapter_name,
 130            has_adapter_logs,
 131            is_terminated: false,
 132        }
 133    }
 134}
 135
 136impl LogStore {
 137    pub fn new(cx: &Context<Self>) -> Self {
 138        let (rpc_tx, mut rpc_rx) = unbounded::<LogStoreMessage>();
 139        cx.spawn(async move |this, cx| {
 140            while let Some(message) = rpc_rx.next().await {
 141                if let Some(this) = this.upgrade() {
 142                    this.update(cx, |this, cx| {
 143                        this.add_debug_adapter_message(message, cx);
 144                    })?;
 145                }
 146
 147                smol::future::yield_now().await;
 148            }
 149            anyhow::Ok(())
 150        })
 151        .detach_and_log_err(cx);
 152
 153        let (adapter_log_tx, mut adapter_log_rx) = unbounded::<LogStoreMessage>();
 154        cx.spawn(async move |this, cx| {
 155            while let Some(message) = adapter_log_rx.next().await {
 156                if let Some(this) = this.upgrade() {
 157                    this.update(cx, |this, cx| {
 158                        this.add_debug_adapter_log(message, cx);
 159                    })?;
 160                }
 161
 162                smol::future::yield_now().await;
 163            }
 164            anyhow::Ok(())
 165        })
 166        .detach_and_log_err(cx);
 167        Self {
 168            rpc_tx,
 169            adapter_log_tx,
 170            projects: HashMap::new(),
 171        }
 172    }
 173
 174    pub fn add_project(&mut self, project: &Entity<Project>, cx: &mut Context<Self>) {
 175        self.projects.insert(
 176            project.downgrade(),
 177            ProjectState {
 178                _subscriptions: [
 179                    cx.observe_release(project, {
 180                        let weak_project = project.downgrade();
 181                        move |this, _, _| {
 182                            this.projects.remove(&weak_project);
 183                        }
 184                    }),
 185                    cx.subscribe(&project.read(cx).dap_store(), {
 186                        let weak_project = project.downgrade();
 187                        move |this, dap_store, event, cx| match event {
 188                            dap_store::DapStoreEvent::DebugClientStarted(session_id) => {
 189                                let session = dap_store.read(cx).session_by_id(session_id);
 190                                if let Some(session) = session {
 191                                    this.add_debug_session(
 192                                        LogStoreEntryIdentifier {
 193                                            project: Cow::Owned(weak_project.clone()),
 194                                            session_id: *session_id,
 195                                        },
 196                                        session,
 197                                        cx,
 198                                    );
 199                                }
 200                            }
 201                            dap_store::DapStoreEvent::DebugClientShutdown(session_id) => {
 202                                let id = LogStoreEntryIdentifier {
 203                                    project: Cow::Borrowed(&weak_project),
 204                                    session_id: *session_id,
 205                                };
 206                                if let Some(state) = this.get_debug_adapter_state(&id) {
 207                                    state.is_terminated = true;
 208                                }
 209
 210                                this.clean_sessions(cx);
 211                            }
 212                            _ => {}
 213                        }
 214                    }),
 215                ],
 216                debug_sessions: Default::default(),
 217            },
 218        );
 219    }
 220
 221    fn get_debug_adapter_state(
 222        &mut self,
 223        id: &LogStoreEntryIdentifier<'_>,
 224    ) -> Option<&mut DebugAdapterState> {
 225        self.projects
 226            .get_mut(&id.project)
 227            .and_then(|state| state.debug_sessions.get_mut(&id.session_id))
 228    }
 229
 230    fn add_debug_adapter_message(
 231        &mut self,
 232        LogStoreMessage {
 233            id,
 234            kind: io_kind,
 235            command,
 236            message,
 237        }: LogStoreMessage,
 238        cx: &mut Context<Self>,
 239    ) {
 240        let Some(debug_client_state) = self.get_debug_adapter_state(&id) else {
 241            return;
 242        };
 243
 244        let is_init_seq = command.as_ref().is_some_and(|command| {
 245            matches!(
 246                command.as_ref(),
 247                "attach" | "launch" | "initialize" | "configurationDone"
 248            )
 249        });
 250
 251        let kind = match io_kind {
 252            IoKind::StdOut | IoKind::StdErr => MessageKind::Receive,
 253            IoKind::StdIn => MessageKind::Send,
 254        };
 255
 256        let rpc_messages = &mut debug_client_state.rpc_messages;
 257
 258        // Push a separator if the kind has changed
 259        if rpc_messages.last_message_kind != Some(kind) {
 260            Self::get_debug_adapter_entry(
 261                &mut rpc_messages.messages,
 262                id.to_owned(),
 263                kind.label().into(),
 264                LogKind::Rpc,
 265                cx,
 266            );
 267            rpc_messages.last_message_kind = Some(kind);
 268        }
 269
 270        let entry = Self::get_debug_adapter_entry(
 271            &mut rpc_messages.messages,
 272            id.to_owned(),
 273            message,
 274            LogKind::Rpc,
 275            cx,
 276        );
 277
 278        if is_init_seq {
 279            if rpc_messages.last_init_message_kind != Some(kind) {
 280                rpc_messages
 281                    .initialization_sequence
 282                    .push(SharedString::from(kind.label()));
 283                rpc_messages.last_init_message_kind = Some(kind);
 284            }
 285            rpc_messages.initialization_sequence.push(entry);
 286        }
 287
 288        cx.notify();
 289    }
 290
 291    fn add_debug_adapter_log(
 292        &mut self,
 293        LogStoreMessage {
 294            id,
 295            kind: io_kind,
 296            message,
 297            ..
 298        }: LogStoreMessage,
 299        cx: &mut Context<Self>,
 300    ) {
 301        let Some(debug_adapter_state) = self.get_debug_adapter_state(&id) else {
 302            return;
 303        };
 304
 305        let message = match io_kind {
 306            IoKind::StdErr => format!("stderr: {message}").into(),
 307            _ => message,
 308        };
 309
 310        Self::get_debug_adapter_entry(
 311            &mut debug_adapter_state.log_messages,
 312            id.to_owned(),
 313            message,
 314            LogKind::Adapter,
 315            cx,
 316        );
 317        cx.notify();
 318    }
 319
 320    fn get_debug_adapter_entry(
 321        log_lines: &mut VecDeque<SharedString>,
 322        id: LogStoreEntryIdentifier<'static>,
 323        message: SharedString,
 324        kind: LogKind,
 325        cx: &mut Context<Self>,
 326    ) -> SharedString {
 327        if let Some(excess) = log_lines
 328            .len()
 329            .checked_sub(RpcMessages::MESSAGE_QUEUE_LIMIT)
 330            && excess > 0
 331        {
 332            log_lines.drain(..excess);
 333        }
 334
 335        let format_messages = DebuggerSettings::get_global(cx).format_dap_log_messages;
 336
 337        let entry = if format_messages {
 338            maybe!({
 339                serde_json::to_string_pretty::<serde_json::Value>(
 340                    &serde_json::from_str(&message).ok()?,
 341                )
 342                .ok()
 343            })
 344            .map(SharedString::from)
 345            .unwrap_or(message)
 346        } else {
 347            message
 348        };
 349        log_lines.push_back(entry.clone());
 350
 351        cx.emit(Event::NewLogEntry {
 352            id,
 353            entry: entry.clone(),
 354            kind,
 355        });
 356
 357        entry
 358    }
 359
 360    fn add_debug_session(
 361        &mut self,
 362        id: LogStoreEntryIdentifier<'static>,
 363        session: Entity<Session>,
 364        cx: &mut Context<Self>,
 365    ) {
 366        maybe!({
 367            let project_entry = self.projects.get_mut(&id.project)?;
 368            let std::collections::btree_map::Entry::Vacant(state) =
 369                project_entry.debug_sessions.entry(id.session_id)
 370            else {
 371                return None;
 372            };
 373
 374            let (adapter_name, has_adapter_logs) = session.read_with(cx, |session, _| {
 375                (
 376                    session.adapter(),
 377                    session
 378                        .adapter_client()
 379                        .map_or(false, |client| client.has_adapter_logs()),
 380                )
 381            });
 382
 383            state.insert(DebugAdapterState::new(
 384                id.session_id,
 385                adapter_name,
 386                has_adapter_logs,
 387            ));
 388
 389            self.clean_sessions(cx);
 390
 391            let io_tx = self.rpc_tx.clone();
 392
 393            let client = session.read(cx).adapter_client()?;
 394            let project = id.project.clone();
 395            let session_id = id.session_id;
 396            client.add_log_handler(
 397                move |kind, command, message| {
 398                    io_tx
 399                        .unbounded_send(LogStoreMessage {
 400                            id: LogStoreEntryIdentifier {
 401                                session_id,
 402                                project: project.clone(),
 403                            },
 404                            kind,
 405                            command: command.map(|command| command.to_owned().into()),
 406                            message: message.to_owned().into(),
 407                        })
 408                        .ok();
 409                },
 410                LogKind::Rpc,
 411            );
 412
 413            let log_io_tx = self.adapter_log_tx.clone();
 414            let project = id.project;
 415            client.add_log_handler(
 416                move |kind, command, message| {
 417                    log_io_tx
 418                        .unbounded_send(LogStoreMessage {
 419                            id: LogStoreEntryIdentifier {
 420                                session_id,
 421                                project: project.clone(),
 422                            },
 423                            kind,
 424                            command: command.map(|command| command.to_owned().into()),
 425                            message: message.to_owned().into(),
 426                        })
 427                        .ok();
 428                },
 429                LogKind::Adapter,
 430            );
 431            Some(())
 432        });
 433    }
 434
 435    fn clean_sessions(&mut self, cx: &mut Context<Self>) {
 436        self.projects.values_mut().for_each(|project| {
 437            let mut allowed_terminated_sessions = 10u32;
 438            project.debug_sessions.retain(|_, session| {
 439                if !session.is_terminated {
 440                    return true;
 441                }
 442                allowed_terminated_sessions = allowed_terminated_sessions.saturating_sub(1);
 443                allowed_terminated_sessions > 0
 444            });
 445        });
 446
 447        cx.notify();
 448    }
 449
 450    fn log_messages_for_session(
 451        &mut self,
 452        id: &LogStoreEntryIdentifier<'_>,
 453    ) -> Option<&mut VecDeque<SharedString>> {
 454        self.get_debug_adapter_state(id)
 455            .map(|state| &mut state.log_messages)
 456    }
 457
 458    fn rpc_messages_for_session(
 459        &mut self,
 460        id: &LogStoreEntryIdentifier<'_>,
 461    ) -> Option<&mut VecDeque<SharedString>> {
 462        self.get_debug_adapter_state(id)
 463            .map(|state| &mut state.rpc_messages.messages)
 464    }
 465
 466    fn initialization_sequence_for_session(
 467        &mut self,
 468        id: &LogStoreEntryIdentifier<'_>,
 469    ) -> Option<&Vec<SharedString>> {
 470        self.get_debug_adapter_state(&id)
 471            .map(|state| &state.rpc_messages.initialization_sequence)
 472    }
 473}
 474
 475pub struct DapLogToolbarItemView {
 476    log_view: Option<Entity<DapLogView>>,
 477}
 478
 479impl DapLogToolbarItemView {
 480    pub fn new() -> Self {
 481        Self { log_view: None }
 482    }
 483}
 484
 485impl Render for DapLogToolbarItemView {
 486    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 487        let Some(log_view) = self.log_view.clone() else {
 488            return Empty.into_any_element();
 489        };
 490
 491        let (menu_rows, current_session_id, project) = log_view.update(cx, |log_view, cx| {
 492            (
 493                log_view.menu_items(cx),
 494                log_view.current_view.map(|(session_id, _)| session_id),
 495                log_view.project.downgrade(),
 496            )
 497        });
 498
 499        let current_client = current_session_id
 500            .and_then(|session_id| menu_rows.iter().find(|row| row.session_id == session_id));
 501
 502        let dap_menu: PopoverMenu<_> = PopoverMenu::new("DapLogView")
 503            .anchor(gpui::Corner::TopLeft)
 504            .trigger(Button::new(
 505                "debug_client_menu_header",
 506                current_client
 507                    .map(|sub_item| {
 508                        Cow::Owned(format!(
 509                            "{} ({}) - {}",
 510                            sub_item.adapter_name,
 511                            sub_item.session_id.0,
 512                            match sub_item.selected_entry {
 513                                LogKind::Adapter => ADAPTER_LOGS,
 514                                LogKind::Rpc => RPC_MESSAGES,
 515                            }
 516                        ))
 517                    })
 518                    .unwrap_or_else(|| "No adapter selected".into()),
 519            ))
 520            .menu(move |mut window, cx| {
 521                let log_view = log_view.clone();
 522                let menu_rows = menu_rows.clone();
 523                let project = project.clone();
 524                ContextMenu::build(&mut window, cx, move |mut menu, window, _cx| {
 525                    for row in menu_rows.into_iter() {
 526                        menu = menu.custom_row(move |_window, _cx| {
 527                            div()
 528                                .w_full()
 529                                .pl_2()
 530                                .child(
 531                                    Label::new(format!(
 532                                        "{}. {}",
 533                                        row.session_id.0, row.adapter_name,
 534                                    ))
 535                                    .color(workspace::ui::Color::Muted),
 536                                )
 537                                .into_any_element()
 538                        });
 539
 540                        if row.has_adapter_logs {
 541                            menu = menu.custom_entry(
 542                                move |_window, _cx| {
 543                                    div()
 544                                        .w_full()
 545                                        .pl_4()
 546                                        .child(Label::new(ADAPTER_LOGS))
 547                                        .into_any_element()
 548                                },
 549                                window.handler_for(&log_view, {
 550                                    let project = project.clone();
 551                                    let id = LogStoreEntryIdentifier {
 552                                        project: Cow::Owned(project),
 553                                        session_id: row.session_id,
 554                                    };
 555                                    move |view, window, cx| {
 556                                        view.show_log_messages_for_adapter(&id, window, cx);
 557                                    }
 558                                }),
 559                            );
 560                        }
 561
 562                        menu = menu
 563                            .custom_entry(
 564                                move |_window, _cx| {
 565                                    div()
 566                                        .w_full()
 567                                        .pl_4()
 568                                        .child(Label::new(RPC_MESSAGES))
 569                                        .into_any_element()
 570                                },
 571                                window.handler_for(&log_view, {
 572                                    let project = project.clone();
 573                                    let id = LogStoreEntryIdentifier {
 574                                        project: Cow::Owned(project),
 575                                        session_id: row.session_id,
 576                                    };
 577                                    move |view, window, cx| {
 578                                        view.show_rpc_trace_for_server(&id, window, cx);
 579                                    }
 580                                }),
 581                            )
 582                            .custom_entry(
 583                                move |_window, _cx| {
 584                                    div()
 585                                        .w_full()
 586                                        .pl_4()
 587                                        .child(Label::new(INITIALIZATION_SEQUENCE))
 588                                        .into_any_element()
 589                                },
 590                                window.handler_for(&log_view, {
 591                                    let project = project.clone();
 592                                    let id = LogStoreEntryIdentifier {
 593                                        project: Cow::Owned(project),
 594                                        session_id: row.session_id,
 595                                    };
 596                                    move |view, window, cx| {
 597                                        view.show_initialization_sequence_for_server(
 598                                            &id, window, cx,
 599                                        );
 600                                    }
 601                                }),
 602                            );
 603                    }
 604
 605                    menu
 606                })
 607                .into()
 608            });
 609
 610        h_flex()
 611            .size_full()
 612            .child(dap_menu)
 613            .child(
 614                div()
 615                    .child(
 616                        Button::new("clear_log_button", "Clear").on_click(cx.listener(
 617                            |this, _, window, cx| {
 618                                if let Some(log_view) = this.log_view.as_ref() {
 619                                    log_view.update(cx, |log_view, cx| {
 620                                        log_view.editor.update(cx, |editor, cx| {
 621                                            editor.set_read_only(false);
 622                                            editor.clear(window, cx);
 623                                            editor.set_read_only(true);
 624                                        });
 625                                    })
 626                                }
 627                            },
 628                        )),
 629                    )
 630                    .ml_2(),
 631            )
 632            .into_any_element()
 633    }
 634}
 635
 636impl EventEmitter<ToolbarItemEvent> for DapLogToolbarItemView {}
 637
 638impl ToolbarItemView for DapLogToolbarItemView {
 639    fn set_active_pane_item(
 640        &mut self,
 641        active_pane_item: Option<&dyn workspace::item::ItemHandle>,
 642        _window: &mut Window,
 643        cx: &mut Context<Self>,
 644    ) -> workspace::ToolbarItemLocation {
 645        if let Some(item) = active_pane_item {
 646            if let Some(log_view) = item.downcast::<DapLogView>() {
 647                self.log_view = Some(log_view.clone());
 648                return workspace::ToolbarItemLocation::PrimaryLeft;
 649            }
 650        }
 651        self.log_view = None;
 652
 653        cx.notify();
 654
 655        workspace::ToolbarItemLocation::Hidden
 656    }
 657}
 658
 659impl DapLogView {
 660    pub fn new(
 661        project: Entity<Project>,
 662        log_store: Entity<LogStore>,
 663        window: &mut Window,
 664        cx: &mut Context<Self>,
 665    ) -> Self {
 666        let (editor, editor_subscriptions) = Self::editor_for_logs(String::new(), window, cx);
 667
 668        let focus_handle = cx.focus_handle();
 669
 670        let events_subscriptions = cx.subscribe(&log_store, |log_view, _, event, cx| match event {
 671            Event::NewLogEntry { id, entry, kind } => {
 672                if log_view.current_view == Some((id.session_id, *kind))
 673                    && log_view.project == *id.project
 674                {
 675                    log_view.editor.update(cx, |editor, cx| {
 676                        editor.set_read_only(false);
 677                        let last_point = editor.buffer().read(cx).len(cx);
 678                        editor.edit(
 679                            vec![
 680                                (last_point..last_point, entry.trim()),
 681                                (last_point..last_point, "\n"),
 682                            ],
 683                            cx,
 684                        );
 685                        editor.set_read_only(true);
 686                    });
 687                }
 688            }
 689        });
 690        let weak_project = project.downgrade();
 691        let state_info = log_store
 692            .read(cx)
 693            .projects
 694            .get(&weak_project)
 695            .and_then(|project| {
 696                project
 697                    .debug_sessions
 698                    .values()
 699                    .next_back()
 700                    .map(|session| (session.id, session.has_adapter_logs))
 701            });
 702
 703        let mut this = Self {
 704            editor,
 705            focus_handle,
 706            project,
 707            log_store,
 708            editor_subscriptions,
 709            current_view: None,
 710            _subscriptions: vec![events_subscriptions],
 711        };
 712
 713        if let Some((session_id, have_adapter_logs)) = state_info {
 714            let id = LogStoreEntryIdentifier {
 715                session_id,
 716                project: Cow::Owned(weak_project),
 717            };
 718            if have_adapter_logs {
 719                this.show_log_messages_for_adapter(&id, window, cx);
 720            } else {
 721                this.show_rpc_trace_for_server(&id, window, cx);
 722            }
 723        }
 724
 725        this
 726    }
 727
 728    fn editor_for_logs(
 729        log_contents: String,
 730        window: &mut Window,
 731        cx: &mut Context<Self>,
 732    ) -> (Entity<Editor>, Vec<Subscription>) {
 733        let editor = cx.new(|cx| {
 734            let mut editor = Editor::multi_line(window, cx);
 735            editor.set_text(log_contents, window, cx);
 736            editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
 737            editor.set_show_code_actions(false, cx);
 738            editor.set_show_breakpoints(false, cx);
 739            editor.set_show_git_diff_gutter(false, cx);
 740            editor.set_show_runnables(false, cx);
 741            editor.set_input_enabled(false);
 742            editor.set_use_autoclose(false);
 743            editor.set_read_only(true);
 744            editor.set_show_edit_predictions(Some(false), window, cx);
 745            editor
 746        });
 747        let editor_subscription = cx.subscribe(
 748            &editor,
 749            |_, _, event: &EditorEvent, cx: &mut Context<DapLogView>| cx.emit(event.clone()),
 750        );
 751        let search_subscription = cx.subscribe(
 752            &editor,
 753            |_, _, event: &SearchEvent, cx: &mut Context<DapLogView>| cx.emit(event.clone()),
 754        );
 755        (editor, vec![editor_subscription, search_subscription])
 756    }
 757
 758    fn menu_items(&self, cx: &App) -> Vec<DapMenuItem> {
 759        self.log_store
 760            .read(cx)
 761            .projects
 762            .get(&self.project.downgrade())
 763            .map_or_else(Vec::new, |state| {
 764                state
 765                    .debug_sessions
 766                    .values()
 767                    .rev()
 768                    .map(|state| DapMenuItem {
 769                        session_id: state.id,
 770                        adapter_name: state.adapter_name.clone(),
 771                        has_adapter_logs: state.has_adapter_logs,
 772                        selected_entry: self
 773                            .current_view
 774                            .map_or(LogKind::Adapter, |(_, kind)| kind),
 775                    })
 776                    .collect::<Vec<_>>()
 777            })
 778    }
 779
 780    fn show_rpc_trace_for_server(
 781        &mut self,
 782        id: &LogStoreEntryIdentifier<'_>,
 783        window: &mut Window,
 784        cx: &mut Context<Self>,
 785    ) {
 786        let rpc_log = self.log_store.update(cx, |log_store, _| {
 787            log_store
 788                .rpc_messages_for_session(id)
 789                .map(|state| log_contents(state.iter().cloned()))
 790        });
 791        if let Some(rpc_log) = rpc_log {
 792            self.current_view = Some((id.session_id, LogKind::Rpc));
 793            let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
 794            let language = self.project.read(cx).languages().language_for_name("JSON");
 795            editor
 796                .read(cx)
 797                .buffer()
 798                .read(cx)
 799                .as_singleton()
 800                .expect("log buffer should be a singleton")
 801                .update(cx, |_, cx| {
 802                    cx.spawn({
 803                        async move |buffer, cx| {
 804                            let language = language.await.ok();
 805                            buffer.update(cx, |buffer, cx| {
 806                                buffer.set_language(language, cx);
 807                            })
 808                        }
 809                    })
 810                    .detach_and_log_err(cx);
 811                });
 812
 813            self.editor = editor;
 814            self.editor_subscriptions = editor_subscriptions;
 815            cx.notify();
 816        }
 817
 818        cx.focus_self(window);
 819    }
 820
 821    fn show_log_messages_for_adapter(
 822        &mut self,
 823        id: &LogStoreEntryIdentifier<'_>,
 824        window: &mut Window,
 825        cx: &mut Context<Self>,
 826    ) {
 827        let message_log = self.log_store.update(cx, |log_store, _| {
 828            log_store
 829                .log_messages_for_session(id)
 830                .map(|state| log_contents(state.iter().cloned()))
 831        });
 832        if let Some(message_log) = message_log {
 833            self.current_view = Some((id.session_id, LogKind::Adapter));
 834            let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, window, cx);
 835            editor
 836                .read(cx)
 837                .buffer()
 838                .read(cx)
 839                .as_singleton()
 840                .expect("log buffer should be a singleton");
 841
 842            self.editor = editor;
 843            self.editor_subscriptions = editor_subscriptions;
 844            cx.notify();
 845        }
 846
 847        cx.focus_self(window);
 848    }
 849
 850    fn show_initialization_sequence_for_server(
 851        &mut self,
 852        id: &LogStoreEntryIdentifier<'_>,
 853        window: &mut Window,
 854        cx: &mut Context<Self>,
 855    ) {
 856        let rpc_log = self.log_store.update(cx, |log_store, _| {
 857            log_store
 858                .initialization_sequence_for_session(id)
 859                .map(|state| log_contents(state.iter().cloned()))
 860        });
 861        if let Some(rpc_log) = rpc_log {
 862            self.current_view = Some((id.session_id, LogKind::Rpc));
 863            let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
 864            let language = self.project.read(cx).languages().language_for_name("JSON");
 865            editor
 866                .read(cx)
 867                .buffer()
 868                .read(cx)
 869                .as_singleton()
 870                .expect("log buffer should be a singleton")
 871                .update(cx, |_, cx| {
 872                    cx.spawn({
 873                        let buffer = cx.entity();
 874                        async move |_, cx| {
 875                            let language = language.await.ok();
 876                            buffer.update(cx, |buffer, cx| {
 877                                buffer.set_language(language, cx);
 878                            })
 879                        }
 880                    })
 881                    .detach_and_log_err(cx);
 882                });
 883
 884            self.editor = editor;
 885            self.editor_subscriptions = editor_subscriptions;
 886            cx.notify();
 887        }
 888
 889        cx.focus_self(window);
 890    }
 891}
 892
 893fn log_contents(lines: impl Iterator<Item = SharedString>) -> String {
 894    lines.fold(String::new(), |mut acc, el| {
 895        acc.push_str(&el);
 896        acc.push('\n');
 897        acc
 898    })
 899}
 900
 901#[derive(Clone, PartialEq)]
 902pub(crate) struct DapMenuItem {
 903    pub session_id: SessionId,
 904    pub adapter_name: DebugAdapterName,
 905    pub has_adapter_logs: bool,
 906    pub selected_entry: LogKind,
 907}
 908
 909const ADAPTER_LOGS: &str = "Adapter Logs";
 910const RPC_MESSAGES: &str = "RPC Messages";
 911const INITIALIZATION_SEQUENCE: &str = "Initialization Sequence";
 912
 913impl Render for DapLogView {
 914    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 915        self.editor.update(cx, |editor, cx| {
 916            editor.render(window, cx).into_any_element()
 917        })
 918    }
 919}
 920
 921actions!(
 922    dev,
 923    [
 924        /// Opens the debug adapter protocol logs viewer.
 925        OpenDebugAdapterLogs
 926    ]
 927);
 928
 929pub fn init(cx: &mut App) {
 930    let log_store = cx.new(|cx| LogStore::new(cx));
 931
 932    cx.observe_new(move |workspace: &mut Workspace, window, cx| {
 933        let Some(_window) = window else {
 934            return;
 935        };
 936
 937        let project = workspace.project();
 938        if project.read(cx).is_local() {
 939            log_store.update(cx, |store, cx| {
 940                store.add_project(project, cx);
 941            });
 942        }
 943
 944        let log_store = log_store.clone();
 945        workspace.register_action(move |workspace, _: &OpenDebugAdapterLogs, window, cx| {
 946            let project = workspace.project().read(cx);
 947            if project.is_local() {
 948                workspace.add_item_to_active_pane(
 949                    Box::new(cx.new(|cx| {
 950                        DapLogView::new(workspace.project().clone(), log_store.clone(), window, cx)
 951                    })),
 952                    None,
 953                    true,
 954                    window,
 955                    cx,
 956                );
 957            }
 958        });
 959    })
 960    .detach();
 961}
 962
 963impl Item for DapLogView {
 964    type Event = EditorEvent;
 965
 966    fn to_item_events(event: &Self::Event, f: impl FnMut(workspace::item::ItemEvent)) {
 967        Editor::to_item_events(event, f)
 968    }
 969
 970    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
 971        "DAP Logs".into()
 972    }
 973
 974    fn telemetry_event_text(&self) -> Option<&'static str> {
 975        None
 976    }
 977
 978    fn as_searchable(&self, handle: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
 979        Some(Box::new(handle.clone()))
 980    }
 981}
 982
 983impl SearchableItem for DapLogView {
 984    type Match = <Editor as SearchableItem>::Match;
 985
 986    fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 987        self.editor.update(cx, |e, cx| e.clear_matches(window, cx))
 988    }
 989
 990    fn update_matches(
 991        &mut self,
 992        matches: &[Self::Match],
 993        window: &mut Window,
 994        cx: &mut Context<Self>,
 995    ) {
 996        self.editor
 997            .update(cx, |e, cx| e.update_matches(matches, window, cx))
 998    }
 999
1000    fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {
1001        self.editor
1002            .update(cx, |e, cx| e.query_suggestion(window, cx))
1003    }
1004
1005    fn activate_match(
1006        &mut self,
1007        index: usize,
1008        matches: &[Self::Match],
1009        window: &mut Window,
1010        cx: &mut Context<Self>,
1011    ) {
1012        self.editor
1013            .update(cx, |e, cx| e.activate_match(index, matches, window, cx))
1014    }
1015
1016    fn select_matches(
1017        &mut self,
1018        matches: &[Self::Match],
1019        window: &mut Window,
1020        cx: &mut Context<Self>,
1021    ) {
1022        self.editor
1023            .update(cx, |e, cx| e.select_matches(matches, window, cx))
1024    }
1025
1026    fn find_matches(
1027        &mut self,
1028        query: Arc<project::search::SearchQuery>,
1029        window: &mut Window,
1030        cx: &mut Context<Self>,
1031    ) -> gpui::Task<Vec<Self::Match>> {
1032        self.editor
1033            .update(cx, |e, cx| e.find_matches(query, window, cx))
1034    }
1035
1036    fn replace(
1037        &mut self,
1038        _: &Self::Match,
1039        _: &SearchQuery,
1040        _window: &mut Window,
1041        _: &mut Context<Self>,
1042    ) {
1043        // Since DAP Log is read-only, it doesn't make sense to support replace operation.
1044    }
1045
1046    fn supported_options(&self) -> workspace::searchable::SearchOptions {
1047        workspace::searchable::SearchOptions {
1048            case: true,
1049            word: true,
1050            regex: true,
1051            find_in_results: true,
1052            // DAP log is read-only.
1053            replacement: false,
1054            selection: false,
1055        }
1056    }
1057    fn active_match_index(
1058        &mut self,
1059        direction: Direction,
1060        matches: &[Self::Match],
1061        window: &mut Window,
1062        cx: &mut Context<Self>,
1063    ) -> Option<usize> {
1064        self.editor.update(cx, |e, cx| {
1065            e.active_match_index(direction, matches, window, cx)
1066        })
1067    }
1068}
1069
1070impl Focusable for DapLogView {
1071    fn focus_handle(&self, _cx: &App) -> FocusHandle {
1072        self.focus_handle.clone()
1073    }
1074}
1075
1076enum Event {
1077    NewLogEntry {
1078        id: LogStoreEntryIdentifier<'static>,
1079        entry: SharedString,
1080        kind: LogKind,
1081    },
1082}
1083
1084impl EventEmitter<Event> for LogStore {}
1085impl EventEmitter<Event> for DapLogView {}
1086impl EventEmitter<EditorEvent> for DapLogView {}
1087impl EventEmitter<SearchEvent> for DapLogView {}
1088
1089#[cfg(any(test, feature = "test-support"))]
1090impl LogStore {
1091    pub fn has_projects(&self) -> bool {
1092        !self.projects.is_empty()
1093    }
1094
1095    pub fn contained_session_ids(&self, project: &WeakEntity<Project>) -> Vec<SessionId> {
1096        self.projects.get(project).map_or(vec![], |state| {
1097            state.debug_sessions.keys().copied().collect()
1098        })
1099    }
1100
1101    pub fn rpc_messages_for_session_id(
1102        &self,
1103        project: &WeakEntity<Project>,
1104        session_id: SessionId,
1105    ) -> Vec<SharedString> {
1106        self.projects.get(&project).map_or(vec![], |state| {
1107            state
1108                .debug_sessions
1109                .get(&session_id)
1110                .expect("This session should exist if a test is calling")
1111                .rpc_messages
1112                .messages
1113                .clone()
1114                .into()
1115        })
1116    }
1117}