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
  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                            .map_or(false, |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                cx,
 539            ))
 540            .menu(move |mut window, cx| {
 541                let log_view = log_view.clone();
 542                let menu_rows = menu_rows.clone();
 543                let project = project.clone();
 544                ContextMenu::build(&mut 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", cx).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            if let Some(log_view) = item.downcast::<DapLogView>() {
 667                self.log_view = Some(log_view.clone());
 668                return workspace::ToolbarItemLocation::PrimaryLeft;
 669            }
 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_and_log_err(cx);
 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        if project.read(cx).is_local() {
 968            log_store.update(cx, |store, cx| {
 969                store.add_project(project, cx);
 970            });
 971        }
 972
 973        let log_store = log_store.clone();
 974        workspace.register_action(move |workspace, _: &OpenDebugAdapterLogs, window, cx| {
 975            let project = workspace.project().read(cx);
 976            if project.is_local() {
 977                workspace.add_item_to_active_pane(
 978                    Box::new(cx.new(|cx| {
 979                        DapLogView::new(workspace.project().clone(), log_store.clone(), window, cx)
 980                    })),
 981                    None,
 982                    true,
 983                    window,
 984                    cx,
 985                );
 986            }
 987        });
 988    })
 989    .detach();
 990}
 991
 992impl Item for DapLogView {
 993    type Event = EditorEvent;
 994
 995    fn to_item_events(event: &Self::Event, f: impl FnMut(workspace::item::ItemEvent)) {
 996        Editor::to_item_events(event, f)
 997    }
 998
 999    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
1000        "DAP Logs".into()
1001    }
1002
1003    fn telemetry_event_text(&self) -> Option<&'static str> {
1004        None
1005    }
1006
1007    fn as_searchable(&self, handle: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
1008        Some(Box::new(handle.clone()))
1009    }
1010}
1011
1012impl SearchableItem for DapLogView {
1013    type Match = <Editor as SearchableItem>::Match;
1014
1015    fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1016        self.editor.update(cx, |e, cx| e.clear_matches(window, cx))
1017    }
1018
1019    fn update_matches(
1020        &mut self,
1021        matches: &[Self::Match],
1022        window: &mut Window,
1023        cx: &mut Context<Self>,
1024    ) {
1025        self.editor
1026            .update(cx, |e, cx| e.update_matches(matches, window, cx))
1027    }
1028
1029    fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {
1030        self.editor
1031            .update(cx, |e, cx| e.query_suggestion(window, cx))
1032    }
1033
1034    fn activate_match(
1035        &mut self,
1036        index: usize,
1037        matches: &[Self::Match],
1038        window: &mut Window,
1039        cx: &mut Context<Self>,
1040    ) {
1041        self.editor
1042            .update(cx, |e, cx| e.activate_match(index, matches, window, cx))
1043    }
1044
1045    fn select_matches(
1046        &mut self,
1047        matches: &[Self::Match],
1048        window: &mut Window,
1049        cx: &mut Context<Self>,
1050    ) {
1051        self.editor
1052            .update(cx, |e, cx| e.select_matches(matches, window, cx))
1053    }
1054
1055    fn find_matches(
1056        &mut self,
1057        query: Arc<project::search::SearchQuery>,
1058        window: &mut Window,
1059        cx: &mut Context<Self>,
1060    ) -> gpui::Task<Vec<Self::Match>> {
1061        self.editor
1062            .update(cx, |e, cx| e.find_matches(query, window, cx))
1063    }
1064
1065    fn replace(
1066        &mut self,
1067        _: &Self::Match,
1068        _: &SearchQuery,
1069        _window: &mut Window,
1070        _: &mut Context<Self>,
1071    ) {
1072        // Since DAP Log is read-only, it doesn't make sense to support replace operation.
1073    }
1074
1075    fn supported_options(&self) -> workspace::searchable::SearchOptions {
1076        workspace::searchable::SearchOptions {
1077            case: true,
1078            word: true,
1079            regex: true,
1080            find_in_results: true,
1081            // DAP log is read-only.
1082            replacement: false,
1083            selection: false,
1084        }
1085    }
1086    fn active_match_index(
1087        &mut self,
1088        direction: Direction,
1089        matches: &[Self::Match],
1090        window: &mut Window,
1091        cx: &mut Context<Self>,
1092    ) -> Option<usize> {
1093        self.editor.update(cx, |e, cx| {
1094            e.active_match_index(direction, matches, window, cx)
1095        })
1096    }
1097}
1098
1099impl Focusable for DapLogView {
1100    fn focus_handle(&self, _cx: &App) -> FocusHandle {
1101        self.focus_handle.clone()
1102    }
1103}
1104
1105enum Event {
1106    NewLogEntry {
1107        id: LogStoreEntryIdentifier<'static>,
1108        entry: SharedString,
1109        kind: LogKind,
1110    },
1111}
1112
1113impl EventEmitter<Event> for LogStore {}
1114impl EventEmitter<Event> for DapLogView {}
1115impl EventEmitter<EditorEvent> for DapLogView {}
1116impl EventEmitter<SearchEvent> for DapLogView {}
1117
1118#[cfg(any(test, feature = "test-support"))]
1119impl LogStore {
1120    pub fn has_projects(&self) -> bool {
1121        !self.projects.is_empty()
1122    }
1123
1124    pub fn contained_session_ids(&self, project: &WeakEntity<Project>) -> Vec<SessionId> {
1125        self.projects.get(project).map_or(vec![], |state| {
1126            state.debug_sessions.keys().copied().collect()
1127        })
1128    }
1129
1130    pub fn rpc_messages_for_session_id(
1131        &self,
1132        project: &WeakEntity<Project>,
1133        session_id: SessionId,
1134    ) -> Vec<SharedString> {
1135        self.projects.get(&project).map_or(vec![], |state| {
1136            state
1137                .debug_sessions
1138                .get(&session_id)
1139                .expect("This session should exist if a test is calling")
1140                .rpc_messages
1141                .messages
1142                .clone()
1143                .into()
1144        })
1145    }
1146}