Anthony EidRestructure tool calld4c58b55 months agoHistory
620 lines · 23.4 KB · Rust
RawDownload
use crate::{AgentTool, ToolCallEventStream, ToolInput};use agent_client_protocol::schema as acp;use anyhow::{Result, anyhow};use dap::SteppingGranularity;use dap::client::SessionId;use gpui::{App, AsyncApp, Entity, SharedString, Task};use project::Project;use project::debugger::breakpoint_store::{    Breakpoint, BreakpointEditAction, BreakpointState, BreakpointWithPosition,};use project::debugger::session::{Session, ThreadId, ThreadStatus};use schemars::JsonSchema;use serde::{Deserialize, Serialize};use std::fmt::Write;use std::sync::Arc;use text::Point;use util::markdown::MarkdownInlineCode; /// Interact with the debugger to control debug sessions, set breakpoints, and inspect program state.////// This tool allows you to:/// - Set and remove breakpoints at specific file locations/// - List all breakpoints in the project/// - List active debug sessions/// - Control execution (continue, pause, step over, step in, step out)/// - Inspect stack traces and variables when stopped at a breakpoint////// <guidelines>/// - Before using debugger controls (continue, pause, step), ensure there is an active debug session/// - When setting breakpoints, use the exact file path as it appears in the project/// - Stack traces and variables are only available when the debugger is stopped at a breakpoint/// - Use `list_sessions` to see available debug sessions before trying to control them/// </guidelines>#[derive(Debug, Serialize, Deserialize, JsonSchema)]pub struct DebuggerToolInput {    /// The debugger operation to perform, including any operation-specific inputs.    pub operation: DebuggerOperation,    /// Optional session ID. If not provided, uses the active session.    #[serde(default)]    pub session_id: Option<u32>,} /// The debugger operation to perform.#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]#[serde(tag = "type", rename_all = "snake_case")]pub enum DebuggerOperation {    /// Set or update a breakpoint at a specific file and line.    SetBreakpoint {        /// The path to the file.        path: String,        /// The 1-based line number.        line: u32,        /// Whether to enable or disable the breakpoint.        #[serde(default)]        enabled: Option<bool>,        /// Optional condition expression that must evaluate to true for the breakpoint to trigger.        #[serde(default)]        condition: Option<String>,        /// Optional log message to output when the breakpoint is hit.        #[serde(default)]        log_message: Option<String>,        /// Optional hit count condition.        #[serde(default)]        hit_condition: Option<String>,    },    /// Remove a breakpoint at a specific file and line.    RemoveBreakpoint {        /// The path to the file.        path: String,        /// The 1-based line number.        line: u32,    },    /// List all breakpoints in the project.    ListBreakpoints,    /// List all active debug sessions.    ListSessions,    /// Continue execution of a paused thread.    Continue {        /// Optional thread ID. If not provided, uses a stopped thread.        #[serde(default)]        thread_id: Option<i64>,    },    /// Pause execution of a running thread.    Pause {        /// Optional thread ID. If not provided, uses a running thread.        #[serde(default)]        thread_id: Option<i64>,    },    /// Step over the current line (execute without entering functions).    StepOver {        /// Optional thread ID. If not provided, uses a stopped thread.        #[serde(default)]        thread_id: Option<i64>,    },    /// Step into the current line (enter function calls).    StepIn {        /// Optional thread ID. If not provided, uses a stopped thread.        #[serde(default)]        thread_id: Option<i64>,    },    /// Step out of the current function.    StepOut {        /// Optional thread ID. If not provided, uses a stopped thread.        #[serde(default)]        thread_id: Option<i64>,    },    /// Get the stack trace for a stopped thread.    GetStackTrace {        /// Optional thread ID. If not provided, uses an appropriate thread.        #[serde(default)]        thread_id: Option<i64>,    },    /// Get variables in the current scope.    GetVariables {        /// Optional thread ID. If not provided, uses a stopped thread.        #[serde(default)]        thread_id: Option<i64>,        /// Optional stack frame index (0 = top of stack).        #[serde(default)]        frame_index: Option<usize>,    },} pub struct DebuggerTool {    project: Entity<Project>,} impl DebuggerTool {    pub fn new(project: Entity<Project>) -> Self {        Self { project }    }     fn find_session(        project: &Entity<Project>,        session_id: Option<u32>,        cx: &App,    ) -> Result<Entity<Session>> {        let dap_store = project.read(cx).dap_store();        let dap_store = dap_store.read(cx);         if let Some(id) = session_id {            dap_store                .session_by_id(SessionId(id))                .ok_or_else(|| anyhow!("No debug session found with ID {}", id))        } else {            if let Some((session, _)) = project.read(cx).active_debug_session(cx) {                return Ok(session);            }            dap_store                .sessions()                .next()                .cloned()                .ok_or_else(|| anyhow!("No active debug session. Start a debug session first."))        }    }     fn find_stopped_thread(        session: &Entity<Session>,        thread_id: Option<i64>,        cx: &mut App,    ) -> Result<ThreadId> {        session.update(cx, |session, cx| {            let threads = session.threads(cx);             if let Some(tid) = thread_id {                let thread_id = ThreadId::from(tid);                if threads                    .iter()                    .any(|(t, _)| ThreadId::from(t.id) == thread_id)                {                    return Ok(thread_id);                }                return Err(anyhow!("Thread {} not found", tid));            }             threads                .iter()                .find(|(_, status)| *status == ThreadStatus::Stopped)                .map(|(t, _)| ThreadId::from(t.id))                .ok_or_else(|| {                    anyhow!("No stopped thread found. The debugger must be paused at a breakpoint.")                })        })    }     fn find_running_thread(        session: &Entity<Session>,        thread_id: Option<i64>,        cx: &mut App,    ) -> Result<ThreadId> {        session.update(cx, |session, cx| {            let threads = session.threads(cx);             if let Some(tid) = thread_id {                let thread_id = ThreadId::from(tid);                if threads                    .iter()                    .any(|(t, _)| ThreadId::from(t.id) == thread_id)                {                    return Ok(thread_id);                }                return Err(anyhow!("Thread {} not found", tid));            }             threads                .iter()                .find(|(_, status)| *status == ThreadStatus::Running)                .map(|(t, _)| ThreadId::from(t.id))                .ok_or_else(|| anyhow!("No running thread found."))        })    }     fn find_any_thread(        session: &Entity<Session>,        thread_id: Option<i64>,        cx: &mut App,    ) -> Result<ThreadId> {        session.update(cx, |session, cx| {            let threads = session.threads(cx);             if let Some(tid) = thread_id {                let thread_id = ThreadId::from(tid);                if threads                    .iter()                    .any(|(t, _)| ThreadId::from(t.id) == thread_id)                {                    return Ok(thread_id);                }                return Err(anyhow!("Thread {} not found", tid));            }             threads                .iter()                .find(|(_, status)| *status == ThreadStatus::Stopped)                .or_else(|| threads.first())                .map(|(t, _)| ThreadId::from(t.id))                .ok_or_else(|| anyhow!("No threads found in the debug session."))        })    }     async fn run_operation(        project: Entity<Project>,        input: DebuggerToolInput,        cx: &mut AsyncApp,    ) -> Result<String> {        let session_id = input.session_id;        match input.operation {            DebuggerOperation::SetBreakpoint {                path,                line,                enabled,                condition,                log_message,                hit_condition,            } => {                let (buffer_task, breakpoint_store, abs_path): (_, _, _) = cx.update(|cx| {                    let project_path = project.read(cx).find_project_path(&path, cx);                    let Some(project_path) = project_path else {                        return Err(anyhow!("Could not find path {} in project", path));                    };                     let breakpoint_store = project.read(cx).breakpoint_store();                    let buffer_task = project.update(cx, |project, cx| {                        project.open_buffer(project_path.clone(), cx)                    });                     let worktree = project                        .read(cx)                        .worktree_for_id(project_path.worktree_id, cx);                    let abs_path = worktree.map(|wt| wt.read(cx).absolutize(&project_path.path));                     Ok((buffer_task, breakpoint_store, abs_path))                })?;                 let buffer = buffer_task.await?;                let abs_path =                    abs_path.ok_or_else(|| anyhow!("Could not determine absolute path"))?;                 Ok(cx.update(|cx| {                    let snapshot = buffer.read(cx).snapshot();                    let row = line.saturating_sub(1);                    let point = Point::new(row, 0);                    let position = snapshot.anchor_before(point);                     let state = match enabled {                        Some(true) | None => BreakpointState::Enabled,                        Some(false) => BreakpointState::Disabled,                    };                     let breakpoint = Breakpoint {                        message: log_message.map(|s| s.into()),                        hit_condition: hit_condition.map(|s| s.into()),                        condition: condition.map(|s| s.into()),                        state,                    };                     let breakpoint_with_position = BreakpointWithPosition {                        position,                        bp: breakpoint,                    };                     breakpoint_store.update(cx, |store, cx| {                        store.toggle_breakpoint(                            buffer,                            breakpoint_with_position,                            BreakpointEditAction::Toggle,                            cx,                        );                    });                     format!("Breakpoint set at {}:{}", abs_path.display(), line)                }))            }            DebuggerOperation::RemoveBreakpoint { path, line } => cx.update(|cx| {                let project = project.read(cx);                let Some(project_path) = project.find_project_path(&path, cx) else {                    return Err(anyhow!("Could not find path {} in project", path));                };                 let worktree = project                    .worktree_for_id(project_path.worktree_id, cx)                    .ok_or_else(|| anyhow!("Worktree not found"))?;                let abs_path = worktree.read(cx).absolutize(&project_path.path);                 let breakpoint_store = project.breakpoint_store();                let row = line.saturating_sub(1);                 let result = breakpoint_store                    .read(cx)                    .breakpoint_at_row(&abs_path, row, cx);                 if let Some((buffer, breakpoint)) = result {                    breakpoint_store.update(cx, |store, cx| {                        store.toggle_breakpoint(                            buffer,                            breakpoint,                            BreakpointEditAction::Toggle,                            cx,                        );                    });                    Ok(format!("Breakpoint removed at {}:{}", path, line))                } else {                    Ok(format!("No breakpoint found at {}:{}", path, line))                }            }),            DebuggerOperation::ListBreakpoints => Ok(cx.update(|cx| {                let breakpoint_store = project.read(cx).breakpoint_store();                let breakpoints = breakpoint_store.read(cx).all_source_breakpoints(cx);                 let mut output = String::new();                if breakpoints.is_empty() {                    output.push_str("No breakpoints set.");                } else {                    writeln!(output, "Breakpoints:").ok();                    for (path, bps) in &breakpoints {                        for bp in bps {                            let state = if bp.state.is_enabled() {                                "enabled"                            } else {                                "disabled"                            };                            let mut details = vec![state.to_string()];                             if let Some(ref cond) = bp.condition {                                details.push(format!("condition: {}", cond));                            }                            if let Some(ref msg) = bp.message {                                details.push(format!("log: {}", msg));                            }                            if let Some(ref hit) = bp.hit_condition {                                details.push(format!("hit: {}", hit));                            }                             writeln!(                                output,                                "  - {}:{} [{}]",                                path.display(),                                bp.row + 1,                                details.join(", ")                            )                            .ok();                        }                    }                }                output            })),            DebuggerOperation::ListSessions => Ok(cx.update(|cx| {                let dap_store = project.read(cx).dap_store();                let sessions: Vec<_> = dap_store.read(cx).sessions().cloned().collect();                 let mut output = String::new();                if sessions.is_empty() {                    output.push_str("No active debug sessions.");                } else {                    writeln!(output, "Debug sessions:").ok();                    for session in sessions {                        let session_ref = session.read(cx);                        let session_id = session_ref.session_id();                        let adapter = session_ref.adapter();                        let label = session_ref.label();                        let is_terminated = session_ref.is_terminated();                         let status = if is_terminated {                            "terminated"                        } else if session_ref.is_building() {                            "building"                        } else {                            "running"                        };                         let label_str = label.as_ref().map(|l| l.as_ref()).unwrap_or("unnamed");                        writeln!(                            output,                            "  - Session {} ({}): {} [{}]",                            session_id.0, adapter, label_str, status                        )                        .ok();                    }                }                output            })),             DebuggerOperation::Continue { thread_id } => cx.update(|cx| {                let session = Self::find_session(&project, session_id, cx)?;                let tid = Self::find_stopped_thread(&session, thread_id, cx)?;                 session.update(cx, |session, cx| {                    session.continue_thread(tid, cx);                });                 Ok(format!("Continued execution of thread {}", tid.0))            }),             DebuggerOperation::Pause { thread_id } => cx.update(|cx| {                let session = Self::find_session(&project, session_id, cx)?;                let tid = Self::find_running_thread(&session, thread_id, cx)?;                 session.update(cx, |session, cx| {                    session.pause_thread(tid, cx);                });                 Ok(format!("Paused thread {}", tid.0))            }),             DebuggerOperation::StepOver { thread_id } => cx.update(|cx| {                let session = Self::find_session(&project, session_id, cx)?;                let tid = Self::find_stopped_thread(&session, thread_id, cx)?;                 session.update(cx, |session, cx| {                    session.step_over(tid, SteppingGranularity::Line, cx);                });                 Ok(format!("Stepped over on thread {}", tid.0))            }),             DebuggerOperation::StepIn { thread_id } => cx.update(|cx| {                let session = Self::find_session(&project, session_id, cx)?;                let tid = Self::find_stopped_thread(&session, thread_id, cx)?;                 session.update(cx, |session, cx| {                    session.step_in(tid, SteppingGranularity::Line, cx);                });                 Ok(format!("Stepped into on thread {}", tid.0))            }),             DebuggerOperation::StepOut { thread_id } => cx.update(|cx| {                let session = Self::find_session(&project, session_id, cx)?;                let tid = Self::find_stopped_thread(&session, thread_id, cx)?;                 session.update(cx, |session, cx| {                    session.step_out(tid, SteppingGranularity::Line, cx);                });                 Ok(format!("Stepped out on thread {}", tid.0))            }),             DebuggerOperation::GetStackTrace { thread_id } => cx.update(|cx| {                let session = Self::find_session(&project, session_id, cx)?;                let tid = Self::find_any_thread(&session, thread_id, cx)?;                 let frames = session.update(cx, |session, cx| session.stack_frames(tid, cx))?;                 let mut output = String::new();                if frames.is_empty() {                    output.push_str("No stack frames available. The thread may be running.");                } else {                    writeln!(output, "Stack trace for thread {}:", tid.0).ok();                    for (i, frame) in frames.iter().enumerate() {                        let location = frame                            .dap                            .source                            .as_ref()                            .and_then(|s| s.path.as_ref())                            .map(|p| format!("{}:{}", p, frame.dap.line))                            .unwrap_or_else(|| "unknown".to_string());                         writeln!(output, "  #{} {} at {}", i, frame.dap.name, location).ok();                    }                }

Lines 1–500 of 620.