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