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