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, SearchToken, 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::Anchor::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_bookmarks(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(
1032        &mut self,
1033        ignore_settings: bool,
1034        window: &mut Window,
1035        cx: &mut Context<Self>,
1036    ) -> String {
1037        self.editor
1038            .update(cx, |e, cx| e.query_suggestion(ignore_settings, window, cx))
1039    }
1040
1041    fn activate_match(
1042        &mut self,
1043        index: usize,
1044        matches: &[Self::Match],
1045        token: SearchToken,
1046        window: &mut Window,
1047        cx: &mut Context<Self>,
1048    ) {
1049        self.editor.update(cx, |e, cx| {
1050            e.activate_match(index, matches, token, window, cx)
1051        })
1052    }
1053
1054    fn select_matches(
1055        &mut self,
1056        matches: &[Self::Match],
1057        token: SearchToken,
1058        window: &mut Window,
1059        cx: &mut Context<Self>,
1060    ) {
1061        self.editor
1062            .update(cx, |e, cx| e.select_matches(matches, token, window, cx))
1063    }
1064
1065    fn find_matches(
1066        &mut self,
1067        query: Arc<project::search::SearchQuery>,
1068        window: &mut Window,
1069        cx: &mut Context<Self>,
1070    ) -> gpui::Task<Vec<Self::Match>> {
1071        self.editor
1072            .update(cx, |e, cx| e.find_matches(query, window, cx))
1073    }
1074
1075    fn replace(
1076        &mut self,
1077        _: &Self::Match,
1078        _: &SearchQuery,
1079        _token: SearchToken,
1080        _window: &mut Window,
1081        _: &mut Context<Self>,
1082    ) {
1083        // Since DAP Log is read-only, it doesn't make sense to support replace operation.
1084    }
1085
1086    fn supported_options(&self) -> workspace::searchable::SearchOptions {
1087        workspace::searchable::SearchOptions {
1088            case: true,
1089            word: true,
1090            regex: true,
1091            find_in_results: true,
1092            // DAP log is read-only.
1093            replacement: false,
1094            selection: false,
1095            select_all: true,
1096        }
1097    }
1098    fn active_match_index(
1099        &mut self,
1100        direction: Direction,
1101        matches: &[Self::Match],
1102        token: SearchToken,
1103        window: &mut Window,
1104        cx: &mut Context<Self>,
1105    ) -> Option<usize> {
1106        self.editor.update(cx, |e, cx| {
1107            e.active_match_index(direction, matches, token, window, cx)
1108        })
1109    }
1110}
1111
1112impl Focusable for DapLogView {
1113    fn focus_handle(&self, _cx: &App) -> FocusHandle {
1114        self.focus_handle.clone()
1115    }
1116}
1117
1118enum Event {
1119    NewLogEntry {
1120        id: LogStoreEntryIdentifier<'static>,
1121        entry: SharedString,
1122        kind: LogKind,
1123    },
1124}
1125
1126impl EventEmitter<Event> for LogStore {}
1127impl EventEmitter<Event> for DapLogView {}
1128impl EventEmitter<EditorEvent> for DapLogView {}
1129impl EventEmitter<SearchEvent> for DapLogView {}
1130
1131#[cfg(any(test, feature = "test-support"))]
1132impl LogStore {
1133    pub fn has_projects(&self) -> bool {
1134        !self.projects.is_empty()
1135    }
1136
1137    pub fn contained_session_ids(&self, project: &WeakEntity<Project>) -> Vec<SessionId> {
1138        self.projects.get(project).map_or(vec![], |state| {
1139            state.debug_sessions.keys().copied().collect()
1140        })
1141    }
1142
1143    pub fn rpc_messages_for_session_id(
1144        &self,
1145        project: &WeakEntity<Project>,
1146        session_id: SessionId,
1147    ) -> Vec<SharedString> {
1148        self.projects.get(project).map_or(vec![], |state| {
1149            state
1150                .debug_sessions
1151                .get(&session_id)
1152                .expect("This session should exist if a test is calling")
1153                .rpc_messages
1154                .messages
1155                .clone()
1156                .into()
1157        })
1158    }
1159}