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                has_adapter_logs,
 404            ));
 405
 406            self.clean_sessions(cx);
 407
 408            let io_tx = self.rpc_tx.clone();
 409
 410            let client = session.read(cx).adapter_client()?;
 411            let project = id.project.clone();
 412            let session_id = id.session_id;
 413            client.add_log_handler(
 414                move |kind, command, message| {
 415                    io_tx
 416                        .unbounded_send(LogStoreMessage {
 417                            id: LogStoreEntryIdentifier {
 418                                session_id,
 419                                project: project.clone(),
 420                            },
 421                            kind,
 422                            command: command.map(|command| command.to_owned().into()),
 423                            message: message.to_owned().into(),
 424                        })
 425                        .ok();
 426                },
 427                LogKind::Rpc,
 428            );
 429
 430            let log_io_tx = self.adapter_log_tx.clone();
 431            let project = id.project;
 432            client.add_log_handler(
 433                move |kind, command, message| {
 434                    log_io_tx
 435                        .unbounded_send(LogStoreMessage {
 436                            id: LogStoreEntryIdentifier {
 437                                session_id,
 438                                project: project.clone(),
 439                            },
 440                            kind,
 441                            command: command.map(|command| command.to_owned().into()),
 442                            message: message.to_owned().into(),
 443                        })
 444                        .ok();
 445                },
 446                LogKind::Adapter,
 447            );
 448            Some(())
 449        });
 450    }
 451
 452    fn clean_sessions(&mut self, cx: &mut Context<Self>) {
 453        self.projects.values_mut().for_each(|project| {
 454            let mut allowed_terminated_sessions = 10u32;
 455            project.debug_sessions.retain(|_, session| {
 456                if !session.is_terminated {
 457                    return true;
 458                }
 459                allowed_terminated_sessions = allowed_terminated_sessions.saturating_sub(1);
 460                allowed_terminated_sessions > 0
 461            });
 462        });
 463
 464        cx.notify();
 465    }
 466
 467    fn log_messages_for_session(
 468        &mut self,
 469        id: &LogStoreEntryIdentifier<'_>,
 470    ) -> Option<&mut VecDeque<SharedString>> {
 471        self.get_debug_adapter_state(id)
 472            .map(|state| &mut state.log_messages)
 473    }
 474
 475    fn rpc_messages_for_session(
 476        &mut self,
 477        id: &LogStoreEntryIdentifier<'_>,
 478    ) -> Option<&mut VecDeque<SharedString>> {
 479        self.get_debug_adapter_state(id)
 480            .map(|state| &mut state.rpc_messages.messages)
 481    }
 482
 483    fn initialization_sequence_for_session(
 484        &mut self,
 485        id: &LogStoreEntryIdentifier<'_>,
 486    ) -> Option<&Vec<SharedString>> {
 487        self.get_debug_adapter_state(&id)
 488            .map(|state| &state.rpc_messages.initialization_sequence)
 489    }
 490}
 491
 492pub struct DapLogToolbarItemView {
 493    log_view: Option<Entity<DapLogView>>,
 494}
 495
 496impl DapLogToolbarItemView {
 497    pub fn new() -> Self {
 498        Self { log_view: None }
 499    }
 500}
 501
 502impl Render for DapLogToolbarItemView {
 503    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 504        let Some(log_view) = self.log_view.clone() else {
 505            return Empty.into_any_element();
 506        };
 507
 508        let (menu_rows, current_session_id, project) = log_view.update(cx, |log_view, cx| {
 509            (
 510                log_view.menu_items(cx),
 511                log_view.current_view.map(|(session_id, _)| session_id),
 512                log_view.project.downgrade(),
 513            )
 514        });
 515
 516        let current_client = current_session_id
 517            .and_then(|session_id| menu_rows.iter().find(|row| row.session_id == session_id));
 518
 519        let dap_menu: PopoverMenu<_> = PopoverMenu::new("DapLogView")
 520            .anchor(gpui::Corner::TopLeft)
 521            .trigger(Button::new(
 522                "debug_client_menu_header",
 523                current_client
 524                    .map(|sub_item| {
 525                        Cow::Owned(format!(
 526                            "{} - {} - {}",
 527                            sub_item.adapter_name,
 528                            sub_item.session_label,
 529                            match sub_item.selected_entry {
 530                                View::AdapterLogs => ADAPTER_LOGS,
 531                                View::RpcMessages => RPC_MESSAGES,
 532                                View::InitializationSequence => INITIALIZATION_SEQUENCE,
 533                            }
 534                        ))
 535                    })
 536                    .unwrap_or_else(|| "No adapter selected".into()),
 537            ))
 538            .menu(move |mut window, cx| {
 539                let log_view = log_view.clone();
 540                let menu_rows = menu_rows.clone();
 541                let project = project.clone();
 542                ContextMenu::build(&mut window, cx, move |mut menu, window, _cx| {
 543                    for row in menu_rows.into_iter() {
 544                        menu = menu.custom_row(move |_window, _cx| {
 545                            div()
 546                                .w_full()
 547                                .pl_2()
 548                                .child(
 549                                    Label::new(format!(
 550                                        "{} - {}",
 551                                        row.adapter_name, row.session_label
 552                                    ))
 553                                    .color(workspace::ui::Color::Muted),
 554                                )
 555                                .into_any_element()
 556                        });
 557
 558                        if row.has_adapter_logs {
 559                            menu = menu.custom_entry(
 560                                move |_window, _cx| {
 561                                    div()
 562                                        .w_full()
 563                                        .pl_4()
 564                                        .child(Label::new(ADAPTER_LOGS))
 565                                        .into_any_element()
 566                                },
 567                                window.handler_for(&log_view, {
 568                                    let project = project.clone();
 569                                    let id = LogStoreEntryIdentifier {
 570                                        project: Cow::Owned(project),
 571                                        session_id: row.session_id,
 572                                    };
 573                                    move |view, window, cx| {
 574                                        view.show_log_messages_for_adapter(&id, window, cx);
 575                                    }
 576                                }),
 577                            );
 578                        }
 579
 580                        menu = menu
 581                            .custom_entry(
 582                                move |_window, _cx| {
 583                                    div()
 584                                        .w_full()
 585                                        .pl_4()
 586                                        .child(Label::new(RPC_MESSAGES))
 587                                        .into_any_element()
 588                                },
 589                                window.handler_for(&log_view, {
 590                                    let project = project.clone();
 591                                    let id = LogStoreEntryIdentifier {
 592                                        project: Cow::Owned(project),
 593                                        session_id: row.session_id,
 594                                    };
 595                                    move |view, window, cx| {
 596                                        view.show_rpc_trace_for_server(&id, window, cx);
 597                                    }
 598                                }),
 599                            )
 600                            .custom_entry(
 601                                move |_window, _cx| {
 602                                    div()
 603                                        .w_full()
 604                                        .pl_4()
 605                                        .child(Label::new(INITIALIZATION_SEQUENCE))
 606                                        .into_any_element()
 607                                },
 608                                window.handler_for(&log_view, {
 609                                    let project = project.clone();
 610                                    let id = LogStoreEntryIdentifier {
 611                                        project: Cow::Owned(project),
 612                                        session_id: row.session_id,
 613                                    };
 614                                    move |view, window, cx| {
 615                                        view.show_initialization_sequence_for_server(
 616                                            &id, window, cx,
 617                                        );
 618                                    }
 619                                }),
 620                            );
 621                    }
 622
 623                    menu
 624                })
 625                .into()
 626            });
 627
 628        h_flex()
 629            .size_full()
 630            .child(dap_menu)
 631            .child(
 632                div()
 633                    .child(
 634                        Button::new("clear_log_button", "Clear").on_click(cx.listener(
 635                            |this, _, window, cx| {
 636                                if let Some(log_view) = this.log_view.as_ref() {
 637                                    log_view.update(cx, |log_view, cx| {
 638                                        log_view.editor.update(cx, |editor, cx| {
 639                                            editor.set_read_only(false);
 640                                            editor.clear(window, cx);
 641                                            editor.set_read_only(true);
 642                                        });
 643                                    })
 644                                }
 645                            },
 646                        )),
 647                    )
 648                    .ml_2(),
 649            )
 650            .into_any_element()
 651    }
 652}
 653
 654impl EventEmitter<ToolbarItemEvent> for DapLogToolbarItemView {}
 655
 656impl ToolbarItemView for DapLogToolbarItemView {
 657    fn set_active_pane_item(
 658        &mut self,
 659        active_pane_item: Option<&dyn workspace::item::ItemHandle>,
 660        _window: &mut Window,
 661        cx: &mut Context<Self>,
 662    ) -> workspace::ToolbarItemLocation {
 663        if let Some(item) = active_pane_item {
 664            if let Some(log_view) = item.downcast::<DapLogView>() {
 665                self.log_view = Some(log_view.clone());
 666                return workspace::ToolbarItemLocation::PrimaryLeft;
 667            }
 668        }
 669        self.log_view = None;
 670
 671        cx.notify();
 672
 673        workspace::ToolbarItemLocation::Hidden
 674    }
 675}
 676
 677impl DapLogView {
 678    pub fn new(
 679        project: Entity<Project>,
 680        log_store: Entity<LogStore>,
 681        window: &mut Window,
 682        cx: &mut Context<Self>,
 683    ) -> Self {
 684        let (editor, editor_subscriptions) = Self::editor_for_logs(String::new(), window, cx);
 685
 686        let focus_handle = cx.focus_handle();
 687
 688        let events_subscriptions = cx.subscribe(&log_store, |log_view, _, event, cx| match event {
 689            Event::NewLogEntry { id, entry, kind } => {
 690                let is_current_view = match (log_view.current_view, *kind) {
 691                    (Some((i, View::AdapterLogs)), LogKind::Adapter)
 692                    | (Some((i, View::RpcMessages)), LogKind::Rpc)
 693                        if i == id.session_id =>
 694                    {
 695                        log_view.project == *id.project
 696                    }
 697                    _ => false,
 698                };
 699                if is_current_view {
 700                    log_view.editor.update(cx, |editor, cx| {
 701                        editor.set_read_only(false);
 702                        let last_point = editor.buffer().read(cx).len(cx);
 703                        editor.edit(
 704                            vec![
 705                                (last_point..last_point, entry.trim()),
 706                                (last_point..last_point, "\n"),
 707                            ],
 708                            cx,
 709                        );
 710                        editor.set_read_only(true);
 711                    });
 712                }
 713            }
 714        });
 715        let weak_project = project.downgrade();
 716        let state_info = log_store
 717            .read(cx)
 718            .projects
 719            .get(&weak_project)
 720            .and_then(|project| {
 721                project
 722                    .debug_sessions
 723                    .values()
 724                    .next_back()
 725                    .map(|session| (session.id, session.has_adapter_logs))
 726            });
 727
 728        let mut this = Self {
 729            editor,
 730            focus_handle,
 731            project,
 732            log_store,
 733            editor_subscriptions,
 734            current_view: None,
 735            _subscriptions: vec![events_subscriptions],
 736        };
 737
 738        if let Some((session_id, have_adapter_logs)) = state_info {
 739            let id = LogStoreEntryIdentifier {
 740                session_id,
 741                project: Cow::Owned(weak_project),
 742            };
 743            if have_adapter_logs {
 744                this.show_log_messages_for_adapter(&id, window, cx);
 745            } else {
 746                this.show_rpc_trace_for_server(&id, window, cx);
 747            }
 748        }
 749
 750        this
 751    }
 752
 753    fn editor_for_logs(
 754        log_contents: String,
 755        window: &mut Window,
 756        cx: &mut Context<Self>,
 757    ) -> (Entity<Editor>, Vec<Subscription>) {
 758        let editor = cx.new(|cx| {
 759            let mut editor = Editor::multi_line(window, cx);
 760            editor.set_text(log_contents, window, cx);
 761            editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
 762            editor.set_show_code_actions(false, cx);
 763            editor.set_show_breakpoints(false, cx);
 764            editor.set_show_git_diff_gutter(false, cx);
 765            editor.set_show_runnables(false, cx);
 766            editor.set_input_enabled(false);
 767            editor.set_use_autoclose(false);
 768            editor.set_read_only(true);
 769            editor.set_show_edit_predictions(Some(false), window, cx);
 770            editor
 771        });
 772        let editor_subscription = cx.subscribe(
 773            &editor,
 774            |_, _, event: &EditorEvent, cx: &mut Context<DapLogView>| cx.emit(event.clone()),
 775        );
 776        let search_subscription = cx.subscribe(
 777            &editor,
 778            |_, _, event: &SearchEvent, cx: &mut Context<DapLogView>| cx.emit(event.clone()),
 779        );
 780        (editor, vec![editor_subscription, search_subscription])
 781    }
 782
 783    fn menu_items(&self, cx: &App) -> Vec<DapMenuItem> {
 784        self.log_store
 785            .read(cx)
 786            .projects
 787            .get(&self.project.downgrade())
 788            .map_or_else(Vec::new, |state| {
 789                state
 790                    .debug_sessions
 791                    .values()
 792                    .rev()
 793                    .map(|state| DapMenuItem {
 794                        session_id: state.id,
 795                        adapter_name: state.adapter_name.clone(),
 796                        session_label: state.session_label.clone(),
 797                        has_adapter_logs: state.has_adapter_logs,
 798                        selected_entry: self
 799                            .current_view
 800                            .map_or(View::AdapterLogs, |(_, kind)| kind),
 801                    })
 802                    .collect::<Vec<_>>()
 803            })
 804    }
 805
 806    fn show_rpc_trace_for_server(
 807        &mut self,
 808        id: &LogStoreEntryIdentifier<'_>,
 809        window: &mut Window,
 810        cx: &mut Context<Self>,
 811    ) {
 812        let rpc_log = self.log_store.update(cx, |log_store, _| {
 813            log_store
 814                .rpc_messages_for_session(id)
 815                .map(|state| log_contents(state.iter().cloned()))
 816        });
 817        if let Some(rpc_log) = rpc_log {
 818            self.current_view = Some((id.session_id, View::RpcMessages));
 819            let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
 820            let language = self.project.read(cx).languages().language_for_name("JSON");
 821            editor
 822                .read(cx)
 823                .buffer()
 824                .read(cx)
 825                .as_singleton()
 826                .expect("log buffer should be a singleton")
 827                .update(cx, |_, cx| {
 828                    cx.spawn({
 829                        async move |buffer, cx| {
 830                            let language = language.await.ok();
 831                            buffer.update(cx, |buffer, cx| {
 832                                buffer.set_language(language, cx);
 833                            })
 834                        }
 835                    })
 836                    .detach_and_log_err(cx);
 837                });
 838
 839            self.editor = editor;
 840            self.editor_subscriptions = editor_subscriptions;
 841            cx.notify();
 842        }
 843
 844        cx.focus_self(window);
 845    }
 846
 847    fn show_log_messages_for_adapter(
 848        &mut self,
 849        id: &LogStoreEntryIdentifier<'_>,
 850        window: &mut Window,
 851        cx: &mut Context<Self>,
 852    ) {
 853        let message_log = self.log_store.update(cx, |log_store, _| {
 854            log_store
 855                .log_messages_for_session(id)
 856                .map(|state| log_contents(state.iter().cloned()))
 857        });
 858        if let Some(message_log) = message_log {
 859            self.current_view = Some((id.session_id, View::AdapterLogs));
 860            let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, window, cx);
 861            editor
 862                .read(cx)
 863                .buffer()
 864                .read(cx)
 865                .as_singleton()
 866                .expect("log buffer should be a singleton");
 867
 868            self.editor = editor;
 869            self.editor_subscriptions = editor_subscriptions;
 870            cx.notify();
 871        }
 872
 873        cx.focus_self(window);
 874    }
 875
 876    fn show_initialization_sequence_for_server(
 877        &mut self,
 878        id: &LogStoreEntryIdentifier<'_>,
 879        window: &mut Window,
 880        cx: &mut Context<Self>,
 881    ) {
 882        let rpc_log = self.log_store.update(cx, |log_store, _| {
 883            log_store
 884                .initialization_sequence_for_session(id)
 885                .map(|state| log_contents(state.iter().cloned()))
 886        });
 887        if let Some(rpc_log) = rpc_log {
 888            self.current_view = Some((id.session_id, View::InitializationSequence));
 889            let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
 890            let language = self.project.read(cx).languages().language_for_name("JSON");
 891            editor
 892                .read(cx)
 893                .buffer()
 894                .read(cx)
 895                .as_singleton()
 896                .expect("log buffer should be a singleton")
 897                .update(cx, |_, cx| {
 898                    cx.spawn({
 899                        let buffer = cx.entity();
 900                        async move |_, cx| {
 901                            let language = language.await.ok();
 902                            buffer.update(cx, |buffer, cx| {
 903                                buffer.set_language(language, cx);
 904                            })
 905                        }
 906                    })
 907                    .detach_and_log_err(cx);
 908                });
 909
 910            self.editor = editor;
 911            self.editor_subscriptions = editor_subscriptions;
 912            cx.notify();
 913        }
 914
 915        cx.focus_self(window);
 916    }
 917}
 918
 919fn log_contents(lines: impl Iterator<Item = SharedString>) -> String {
 920    lines.fold(String::new(), |mut acc, el| {
 921        acc.push_str(&el);
 922        acc.push('\n');
 923        acc
 924    })
 925}
 926
 927#[derive(Clone, PartialEq)]
 928struct DapMenuItem {
 929    session_id: SessionId,
 930    session_label: SharedString,
 931    adapter_name: DebugAdapterName,
 932    has_adapter_logs: bool,
 933    selected_entry: View,
 934}
 935
 936const ADAPTER_LOGS: &str = "Adapter Logs";
 937const RPC_MESSAGES: &str = "RPC Messages";
 938const INITIALIZATION_SEQUENCE: &str = "Initialization Sequence";
 939
 940impl Render for DapLogView {
 941    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 942        self.editor.update(cx, |editor, cx| {
 943            editor.render(window, cx).into_any_element()
 944        })
 945    }
 946}
 947
 948actions!(
 949    dev,
 950    [
 951        /// Opens the debug adapter protocol logs viewer.
 952        OpenDebugAdapterLogs
 953    ]
 954);
 955
 956pub fn init(cx: &mut App) {
 957    let log_store = cx.new(|cx| LogStore::new(cx));
 958
 959    cx.observe_new(move |workspace: &mut Workspace, window, cx| {
 960        let Some(_window) = window else {
 961            return;
 962        };
 963
 964        let project = workspace.project();
 965        if project.read(cx).is_local() {
 966            log_store.update(cx, |store, cx| {
 967                store.add_project(project, cx);
 968            });
 969        }
 970
 971        let log_store = log_store.clone();
 972        workspace.register_action(move |workspace, _: &OpenDebugAdapterLogs, window, cx| {
 973            let project = workspace.project().read(cx);
 974            if project.is_local() {
 975                workspace.add_item_to_active_pane(
 976                    Box::new(cx.new(|cx| {
 977                        DapLogView::new(workspace.project().clone(), log_store.clone(), window, cx)
 978                    })),
 979                    None,
 980                    true,
 981                    window,
 982                    cx,
 983                );
 984            }
 985        });
 986    })
 987    .detach();
 988}
 989
 990impl Item for DapLogView {
 991    type Event = EditorEvent;
 992
 993    fn to_item_events(event: &Self::Event, f: impl FnMut(workspace::item::ItemEvent)) {
 994        Editor::to_item_events(event, f)
 995    }
 996
 997    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
 998        "DAP Logs".into()
 999    }
1000
1001    fn telemetry_event_text(&self) -> Option<&'static str> {
1002        None
1003    }
1004
1005    fn as_searchable(&self, handle: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
1006        Some(Box::new(handle.clone()))
1007    }
1008}
1009
1010impl SearchableItem for DapLogView {
1011    type Match = <Editor as SearchableItem>::Match;
1012
1013    fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1014        self.editor.update(cx, |e, cx| e.clear_matches(window, cx))
1015    }
1016
1017    fn update_matches(
1018        &mut self,
1019        matches: &[Self::Match],
1020        window: &mut Window,
1021        cx: &mut Context<Self>,
1022    ) {
1023        self.editor
1024            .update(cx, |e, cx| e.update_matches(matches, window, cx))
1025    }
1026
1027    fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {
1028        self.editor
1029            .update(cx, |e, cx| e.query_suggestion(window, cx))
1030    }
1031
1032    fn activate_match(
1033        &mut self,
1034        index: usize,
1035        matches: &[Self::Match],
1036        window: &mut Window,
1037        cx: &mut Context<Self>,
1038    ) {
1039        self.editor
1040            .update(cx, |e, cx| e.activate_match(index, matches, window, cx))
1041    }
1042
1043    fn select_matches(
1044        &mut self,
1045        matches: &[Self::Match],
1046        window: &mut Window,
1047        cx: &mut Context<Self>,
1048    ) {
1049        self.editor
1050            .update(cx, |e, cx| e.select_matches(matches, window, cx))
1051    }
1052
1053    fn find_matches(
1054        &mut self,
1055        query: Arc<project::search::SearchQuery>,
1056        window: &mut Window,
1057        cx: &mut Context<Self>,
1058    ) -> gpui::Task<Vec<Self::Match>> {
1059        self.editor
1060            .update(cx, |e, cx| e.find_matches(query, window, cx))
1061    }
1062
1063    fn replace(
1064        &mut self,
1065        _: &Self::Match,
1066        _: &SearchQuery,
1067        _window: &mut Window,
1068        _: &mut Context<Self>,
1069    ) {
1070        // Since DAP Log is read-only, it doesn't make sense to support replace operation.
1071    }
1072
1073    fn supported_options(&self) -> workspace::searchable::SearchOptions {
1074        workspace::searchable::SearchOptions {
1075            case: true,
1076            word: true,
1077            regex: true,
1078            find_in_results: true,
1079            // DAP log is read-only.
1080            replacement: false,
1081            selection: false,
1082        }
1083    }
1084    fn active_match_index(
1085        &mut self,
1086        direction: Direction,
1087        matches: &[Self::Match],
1088        window: &mut Window,
1089        cx: &mut Context<Self>,
1090    ) -> Option<usize> {
1091        self.editor.update(cx, |e, cx| {
1092            e.active_match_index(direction, matches, window, cx)
1093        })
1094    }
1095}
1096
1097impl Focusable for DapLogView {
1098    fn focus_handle(&self, _cx: &App) -> FocusHandle {
1099        self.focus_handle.clone()
1100    }
1101}
1102
1103enum Event {
1104    NewLogEntry {
1105        id: LogStoreEntryIdentifier<'static>,
1106        entry: SharedString,
1107        kind: LogKind,
1108    },
1109}
1110
1111impl EventEmitter<Event> for LogStore {}
1112impl EventEmitter<Event> for DapLogView {}
1113impl EventEmitter<EditorEvent> for DapLogView {}
1114impl EventEmitter<SearchEvent> for DapLogView {}
1115
1116#[cfg(any(test, feature = "test-support"))]
1117impl LogStore {
1118    pub fn has_projects(&self) -> bool {
1119        !self.projects.is_empty()
1120    }
1121
1122    pub fn contained_session_ids(&self, project: &WeakEntity<Project>) -> Vec<SessionId> {
1123        self.projects.get(project).map_or(vec![], |state| {
1124            state.debug_sessions.keys().copied().collect()
1125        })
1126    }
1127
1128    pub fn rpc_messages_for_session_id(
1129        &self,
1130        project: &WeakEntity<Project>,
1131        session_id: SessionId,
1132    ) -> Vec<SharedString> {
1133        self.projects.get(&project).map_or(vec![], |state| {
1134            state
1135                .debug_sessions
1136                .get(&session_id)
1137                .expect("This session should exist if a test is calling")
1138                .rpc_messages
1139                .messages
1140                .clone()
1141                .into()
1142        })
1143    }
1144}