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. Load more
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(); } }