1038 lines · 32.8 KB · Rust
RawDownload
use dap::{    adapters::DebugAdapterName,    client::SessionId,    debugger_settings::DebuggerSettings,    transport::{IoKind, LogKind},};use editor::{Editor, EditorEvent};use futures::{    StreamExt,    channel::mpsc::{UnboundedSender, unbounded},};use gpui::{    App, AppContext, Context, Empty, Entity, EventEmitter, FocusHandle, Focusable, IntoElement,    ParentElement, Render, SharedString, Styled, Subscription, WeakEntity, Window, actions, div,};use project::{    Project,    debugger::{dap_store, session::Session},    search::SearchQuery,};use settings::Settings as _;use std::{    borrow::Cow,    collections::{HashMap, VecDeque},    sync::Arc,};use util::maybe;use workspace::{    ToolbarItemEvent, ToolbarItemView, Workspace,    item::Item,    searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},    ui::{Button, Clickable, ContextMenu, Label, LabelCommon, PopoverMenu, h_flex},}; // TODO:// - [x] stop sorting by session ID// - [x] pick the most recent session by default (logs if available, RPC messages otherwise)// - [ ] dump the launch/attach request somewhere (logs?) const MAX_SESSIONS: usize = 10; struct DapLogView {    editor: Entity<Editor>,    focus_handle: FocusHandle,    log_store: Entity<LogStore>,    editor_subscriptions: Vec<Subscription>,    current_view: Option<(SessionId, LogKind)>,    project: Entity<Project>,    _subscriptions: Vec<Subscription>,} pub struct LogStore {    projects: HashMap<WeakEntity<Project>, ProjectState>,    debug_sessions: VecDeque<DebugAdapterState>,    rpc_tx: UnboundedSender<(SessionId, IoKind, Option<SharedString>, SharedString)>,    adapter_log_tx: UnboundedSender<(SessionId, IoKind, Option<SharedString>, SharedString)>,} struct ProjectState {    _subscriptions: [gpui::Subscription; 2],} struct DebugAdapterState {    id: SessionId,    log_messages: VecDeque<SharedString>,    rpc_messages: RpcMessages,    adapter_name: DebugAdapterName,    has_adapter_logs: bool,    is_terminated: bool,} struct RpcMessages {    messages: VecDeque<SharedString>,    last_message_kind: Option<MessageKind>,    initialization_sequence: Vec<SharedString>,    last_init_message_kind: Option<MessageKind>,} impl RpcMessages {    const MESSAGE_QUEUE_LIMIT: usize = 255;     fn new() -> Self {        Self {            last_message_kind: None,            last_init_message_kind: None,            messages: VecDeque::with_capacity(Self::MESSAGE_QUEUE_LIMIT),            initialization_sequence: Vec::new(),        }    }} const SEND: &str = "// Send";const RECEIVE: &str = "// Receive"; #[derive(Clone, Copy, PartialEq, Eq)]enum MessageKind {    Send,    Receive,} impl MessageKind {    fn label(&self) -> &'static str {        match self {            Self::Send => SEND,            Self::Receive => RECEIVE,        }    }} impl DebugAdapterState {    fn new(id: SessionId, adapter_name: DebugAdapterName, has_adapter_logs: bool) -> Self {        Self {            id,            log_messages: VecDeque::new(),            rpc_messages: RpcMessages::new(),            adapter_name,            has_adapter_logs,            is_terminated: false,        }    }} impl LogStore {    pub fn new(cx: &Context<Self>) -> Self {        let (rpc_tx, mut rpc_rx) =            unbounded::<(SessionId, IoKind, Option<SharedString>, SharedString)>();        cx.spawn(async move |this, cx| {            while let Some((session_id, io_kind, command, message)) = rpc_rx.next().await {                if let Some(this) = this.upgrade() {                    this.update(cx, |this, cx| {                        this.add_debug_adapter_message(session_id, io_kind, command, message, cx);                    })?;                }                 smol::future::yield_now().await;            }            anyhow::Ok(())        })        .detach_and_log_err(cx);         let (adapter_log_tx, mut adapter_log_rx) =            unbounded::<(SessionId, IoKind, Option<SharedString>, SharedString)>();        cx.spawn(async move |this, cx| {            while let Some((session_id, io_kind, _, message)) = adapter_log_rx.next().await {                if let Some(this) = this.upgrade() {                    this.update(cx, |this, cx| {                        this.add_debug_adapter_log(session_id, io_kind, message, cx);                    })?;                }                 smol::future::yield_now().await;            }            anyhow::Ok(())        })        .detach_and_log_err(cx);        Self {            rpc_tx,            adapter_log_tx,            projects: HashMap::new(),            debug_sessions: Default::default(),        }    }     pub fn add_project(&mut self, project: &Entity<Project>, cx: &mut Context<Self>) {        let weak_project = project.downgrade();        self.projects.insert(            project.downgrade(),            ProjectState {                _subscriptions: [                    cx.observe_release(project, move |this, _, _| {                        this.projects.remove(&weak_project);                    }),                    cx.subscribe(                        &project.read(cx).dap_store(),                        |this, dap_store, event, cx| match event {                            dap_store::DapStoreEvent::DebugClientStarted(session_id) => {                                let session = dap_store.read(cx).session_by_id(session_id);                                if let Some(session) = session {                                    this.add_debug_session(*session_id, session, cx);                                }                            }                            dap_store::DapStoreEvent::DebugClientShutdown(session_id) => {                                this.get_debug_adapter_state(*session_id)                                    .iter_mut()                                    .for_each(|state| state.is_terminated = true);                                this.clean_sessions(cx);                            }                            _ => {}                        },                    ),                ],            },        );    }     fn get_debug_adapter_state(&mut self, id: SessionId) -> Option<&mut DebugAdapterState> {        self.debug_sessions            .iter_mut()            .find(|adapter_state| adapter_state.id == id)    }     fn add_debug_adapter_message(        &mut self,        id: SessionId,        io_kind: IoKind,        command: Option<SharedString>,        message: SharedString,        cx: &mut Context<Self>,    ) {        let Some(debug_client_state) = self.get_debug_adapter_state(id) else {            return;        };         let is_init_seq = command.as_ref().is_some_and(|command| {            matches!(                command.as_ref(),                "attach" | "launch" | "initialize" | "configurationDone"            )        });         let kind = match io_kind {            IoKind::StdOut | IoKind::StdErr => MessageKind::Receive,            IoKind::StdIn => MessageKind::Send,        };         let rpc_messages = &mut debug_client_state.rpc_messages;         // Push a separator if the kind has changed        if rpc_messages.last_message_kind != Some(kind) {            Self::get_debug_adapter_entry(                &mut rpc_messages.messages,                id,                kind.label().into(),                LogKind::Rpc,                cx,            );            rpc_messages.last_message_kind = Some(kind);        }         let entry = Self::get_debug_adapter_entry(            &mut rpc_messages.messages,            id,            message,            LogKind::Rpc,            cx,        );         if is_init_seq {            if rpc_messages.last_init_message_kind != Some(kind) {                rpc_messages                    .initialization_sequence                    .push(SharedString::from(kind.label()));                rpc_messages.last_init_message_kind = Some(kind);            }            rpc_messages.initialization_sequence.push(entry);        }         cx.notify();    }     fn add_debug_adapter_log(        &mut self,        id: SessionId,        io_kind: IoKind,        message: SharedString,        cx: &mut Context<Self>,    ) {        let Some(debug_adapter_state) = self.get_debug_adapter_state(id) else {            return;        };         let message = match io_kind {            IoKind::StdErr => format!("stderr: {message}").into(),            _ => message,        };         Self::get_debug_adapter_entry(            &mut debug_adapter_state.log_messages,            id,            message,            LogKind::Adapter,            cx,        );        cx.notify();    }     fn get_debug_adapter_entry(        log_lines: &mut VecDeque<SharedString>,        id: SessionId,        message: SharedString,        kind: LogKind,        cx: &mut Context<Self>,    ) -> SharedString {        while log_lines.len() >= RpcMessages::MESSAGE_QUEUE_LIMIT {            log_lines.pop_front();        }         let format_messages = DebuggerSettings::get_global(cx).format_dap_log_messages;         let entry = if format_messages {            maybe!({                serde_json::to_string_pretty::<serde_json::Value>(                    &serde_json::from_str(&message).ok()?,                )                .ok()            })            .map(SharedString::from)            .unwrap_or(message)        } else {            message        };        log_lines.push_back(entry.clone());         cx.emit(Event::NewLogEntry {            id,            entry: entry.clone(),            kind,        });         entry    }     fn add_debug_session(        &mut self,        session_id: SessionId,        session: Entity<Session>,        cx: &mut Context<Self>,    ) {        if self            .debug_sessions            .iter_mut()            .any(|adapter_state| adapter_state.id == session_id)        {            return;        }         let (adapter_name, has_adapter_logs) = session.read_with(cx, |session, _| {            (                session.adapter(),                session                    .adapter_client()                    .map(|client| client.has_adapter_logs())                    .unwrap_or(false),            )        });         self.debug_sessions.push_back(DebugAdapterState::new(            session_id,            adapter_name,            has_adapter_logs,        ));         self.clean_sessions(cx);         let io_tx = self.rpc_tx.clone();         let Some(client) = session.read(cx).adapter_client() else {            return;        };         client.add_log_handler(            move |io_kind, command, message| {                io_tx                    .unbounded_send((                        session_id,                        io_kind,                        command.map(|command| command.to_owned().into()),                        message.to_owned().into(),                    ))                    .ok();            },            LogKind::Rpc,        );         let log_io_tx = self.adapter_log_tx.clone();        client.add_log_handler(            move |io_kind, command, message| {                log_io_tx                    .unbounded_send((                        session_id,                        io_kind,                        command.map(|command| command.to_owned().into()),                        message.to_owned().into(),                    ))                    .ok();            },            LogKind::Adapter,        );    }     fn clean_sessions(&mut self, cx: &mut Context<Self>) {        let mut to_remove = self.debug_sessions.len().saturating_sub(MAX_SESSIONS);        self.debug_sessions.retain(|session| {            if to_remove > 0 && session.is_terminated {                to_remove -= 1;                return false;            }            true        });        cx.notify();    }     fn log_messages_for_session(        &mut self,        session_id: SessionId,    ) -> Option<&mut VecDeque<SharedString>> {        self.debug_sessions            .iter_mut()            .find(|session| session.id == session_id)            .map(|state| &mut state.log_messages)    }     fn rpc_messages_for_session(        &mut self,        session_id: SessionId,    ) -> Option<&mut VecDeque<SharedString>> {        self.debug_sessions.iter_mut().find_map(|state| {            if state.id == session_id {                Some(&mut state.rpc_messages.messages)            } else {                None            }        })    }     fn initialization_sequence_for_session(        &mut self,        session_id: SessionId,    ) -> Option<&mut Vec<SharedString>> {        self.debug_sessions.iter_mut().find_map(|state| {            if state.id == session_id {                Some(&mut state.rpc_messages.initialization_sequence)            } else {                None            }        })    }} pub struct DapLogToolbarItemView {    log_view: Option<Entity<DapLogView>>,} impl DapLogToolbarItemView {    pub fn new() -> Self {        Self { log_view: None }    }} impl Render for DapLogToolbarItemView {    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {        let Some(log_view) = self.log_view.clone() else {            return Empty.into_any_element();        };         let (menu_rows, current_session_id) = log_view.update(cx, |log_view, cx| {            (                log_view.menu_items(cx),                log_view.current_view.map(|(session_id, _)| session_id),            )        });         let current_client = current_session_id            .and_then(|session_id| menu_rows.iter().find(|row| row.session_id == session_id));         let dap_menu: PopoverMenu<_> = PopoverMenu::new("DapLogView")            .anchor(gpui::Corner::TopLeft)            .trigger(Button::new(                "debug_client_menu_header",                current_client                    .map(|sub_item| {                        Cow::Owned(format!(                            "{} ({}) - {}",                            sub_item.adapter_name,                            sub_item.session_id.0,                            match sub_item.selected_entry {                                LogKind::Adapter => ADAPTER_LOGS,                                LogKind::Rpc => RPC_MESSAGES,                            }                        ))                    })                    .unwrap_or_else(|| "No adapter selected".into()),            ))            .menu(move |mut window, cx| {                let log_view = log_view.clone();                let menu_rows = menu_rows.clone();                ContextMenu::build(&mut window, cx, move |mut menu, window, _cx| {                    for row in menu_rows.into_iter() {                        menu = menu.custom_row(move |_window, _cx| {                            div()                                .w_full()                                .pl_2()                                .child(                                    Label::new(format!(                                        "{}. {}",                                        row.session_id.0, row.adapter_name,                                    ))                                    .color(workspace::ui::Color::Muted),                                )                                .into_any_element()

Lines 1–500 of 1038.