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