perl-dap 0.13.1

Debug Adapter Protocol server for Perl
//! Stack frame management: stack trace parsing, scopes.

use super::*;

impl DebugAdapter {
    /// Handle stackTrace request
    pub(super) fn handle_stack_trace(
        &self,
        seq: i64,
        request_seq: i64,
        arguments: Option<Value>,
    ) -> DapMessage {
        let args: Option<StackTraceArguments> =
            arguments.and_then(|v| serde_json::from_value(v).ok());
        let start_frame =
            args.as_ref().and_then(|value| value.start_frame).unwrap_or(0).max(0) as usize;
        let levels = args.as_ref().and_then(|value| value.levels).unwrap_or(0);
        let requested_count = if levels <= 0 { None } else { Some(levels as usize) };
        let mut framed_output_lines = None;

        // Ask the debugger for an explicit stack snapshot when a live session is present.
        if let Some(ref mut session) = *lock_or_recover(&self.session, "debug_adapter.session")
            && let Some(stdin) = session.process.stdin.as_mut()
        {
            let commands = vec!["T".to_string()];
            match self.send_framed_debugger_commands(stdin, &commands) {
                Ok((begin, end)) => {
                    framed_output_lines = self.capture_framed_debugger_output(
                        &begin,
                        &end,
                        DEBUGGER_QUERY_WAIT_MS * 8,
                    );
                }
                Err(error) => {
                    tracing::warn!(%error, "Failed to send framed stackTrace command, falling back");
                    let _ = stdin.write_all(b"T\n");
                    let _ = stdin.flush();
                    Self::wait_for_debugger_output_window(DEBUGGER_QUERY_WAIT_MS as u32);
                }
            }
        }

        let parsed_frames = if let Some(lines) = framed_output_lines.as_ref() {
            let output = lines.join("\n");
            let framed_frames =
                Self::filter_user_visible_frames(Self::parse_stack_frames_from_text(&output));
            if framed_frames.is_empty() {
                let output_lines = self.snapshot_recent_output_lines();
                if output_lines.is_empty() {
                    Vec::new()
                } else {
                    let output = output_lines.join("\n");
                    Self::filter_user_visible_frames(Self::parse_stack_frames_from_text(&output))
                }
            } else {
                framed_frames
            }
        } else {
            let output_lines = self.snapshot_recent_output_lines();
            if output_lines.is_empty() {
                Vec::new()
            } else {
                let output = output_lines.join("\n");
                Self::filter_user_visible_frames(Self::parse_stack_frames_from_text(&output))
            }
        };

        let stack_frames = if !parsed_frames.is_empty() {
            // Keep parsed frames as best-effort latest snapshot.
            if let Some(ref mut session) = *lock_or_recover(&self.session, "debug_adapter.session")
            {
                session.stack_frames = parsed_frames.clone();
            }
            parsed_frames
        } else if let Some(ref session) = *lock_or_recover(&self.session, "debug_adapter.session") {
            Self::filter_user_visible_frames(session.stack_frames.clone())
        } else if let Some(pid) = *lock_or_recover(&self.attached_pid, "debug_adapter.attached_pid")
        {
            vec![StackFrame {
                id: Self::i64_to_i32_saturating(i64::from(pid)),
                name: format!("attached::process::{pid}"),
                source: Source {
                    name: Some(format!("pid:{pid}")),
                    path: format!("pid://{pid}"),
                    source_reference: None,
                },
                line: 1,
                column: 1,
                end_line: None,
                end_column: None,
            }]
        } else {
            // No session - return placeholder frame for testing
            vec![StackFrame {
                id: 1,
                name: "main::hello".to_string(),
                source: Source {
                    name: Some("hello.pl".to_string()),
                    path: "/tmp/hello.pl".to_string(),
                    source_reference: None,
                },
                line: 10,
                column: 1,
                end_line: None,
                end_column: None,
            }]
        };
        let stack_frames = Self::paginate_stack_frames(stack_frames, start_frame, requested_count);

        DapMessage::Response {
            seq,
            request_seq,
            success: true,
            command: "stackTrace".to_string(),
            body: Some(json!({
                "stackFrames": stack_frames,
                "totalFrames": stack_frames.len()
            })),
            message: None,
        }
    }

    /// Handle scopes request
    pub(super) fn handle_scopes(
        &self,
        seq: i64,
        request_seq: i64,
        arguments: Option<Value>,
    ) -> DapMessage {
        let args: ScopesArguments = match arguments.and_then(|v| serde_json::from_value(v).ok()) {
            Some(a) => a,
            None => {
                return DapMessage::Response {
                    seq,
                    request_seq,
                    success: false,
                    command: "scopes".to_string(),
                    body: None,
                    message: Some("Missing frameId".to_string()),
                };
            }
        };

        let frame_id = args.frame_id as i32;

        // AC8.3: Hierarchical scope inspection
        // Use bit-shifting or offsets to distinguish between scope types for the same frame
        let locals_ref = frame_id * 10 + 1;
        let package_ref = frame_id * 10 + 2;
        let globals_ref = frame_id * 10 + 3;

        let scopes_body = ScopesResponseBody {
            scopes: vec![
                Scope {
                    name: "Locals".to_string(),
                    presentation_hint: Some("locals".to_string()),
                    variables_reference: i64::from(locals_ref),
                    expensive: false,
                },
                Scope {
                    name: "Package".to_string(),
                    presentation_hint: None,
                    variables_reference: i64::from(package_ref),
                    expensive: true,
                },
                Scope {
                    name: "Globals".to_string(),
                    presentation_hint: None,
                    variables_reference: i64::from(globals_ref),
                    expensive: true,
                },
            ],
        };

        DapMessage::Response {
            seq,
            request_seq,
            success: true,
            command: "scopes".to_string(),
            body: serde_json::to_value(&scopes_body).ok(),
            message: None,
        }
    }
}

impl DebugAdapter {
    fn paginate_stack_frames(
        stack_frames: Vec<StackFrame>,
        start_frame: usize,
        levels: Option<usize>,
    ) -> Vec<StackFrame> {
        let iter = stack_frames.into_iter().skip(start_frame);
        match levels {
            Some(limit) => iter.take(limit).collect(),
            None => iter.collect(),
        }
    }
}