tokensave 7.2.0

Code intelligence tool that builds a semantic knowledge graph from Rust, Go, Java, Scala, TypeScript, Python, C, C++, Kotlin, C#, Swift, and many more codebases
use std::collections::HashMap;
use std::fmt::Write as _;

use crate::types::TaskContext;

/// Markdown fence language for a source file path, derived from its extension.
/// Unknown extensions produce an unlabeled fence.
fn fence_language(file_path: &str) -> &'static str {
    let ext = file_path
        .rsplit('.')
        .next()
        .unwrap_or("")
        .to_ascii_lowercase();
    match ext.as_str() {
        "rs" => "rust",
        "ts" | "mts" | "cts" => "typescript",
        "tsx" => "tsx",
        "js" | "mjs" | "cjs" => "javascript",
        "jsx" => "jsx",
        "py" => "python",
        "go" => "go",
        "java" => "java",
        "kt" | "kts" => "kotlin",
        "rb" => "ruby",
        "php" => "php",
        "cs" => "csharp",
        "c" | "h" => "c",
        "cpp" | "cc" | "cxx" | "hpp" => "cpp",
        "swift" => "swift",
        "dart" => "dart",
        "scala" => "scala",
        "sh" | "bash" | "zsh" => "bash",
        "sql" => "sql",
        "toml" => "toml",
        "yaml" | "yml" => "yaml",
        "json" => "json",
        "html" => "html",
        "css" => "css",
        "lua" => "lua",
        "ex" | "exs" => "elixir",
        "hs" => "haskell",
        "zig" => "zig",
        "vue" => "vue",
        "svelte" => "svelte",
        _ => "",
    }
}

/// Formats a `TaskContext` as a Markdown document suitable for LLM consumption.
///
/// The output includes sections for the query, entry points, related symbols
/// grouped by file, and extracted code blocks.
pub fn format_context_as_markdown(context: &TaskContext) -> String {
    debug_assert!(
        !context.query.is_empty(),
        "format_context_as_markdown called with empty query"
    );
    debug_assert!(
        !context.summary.is_empty(),
        "format_context_as_markdown called with empty summary"
    );
    let mut out = String::new();

    out.push_str("## Code Context\n");
    let _ = write!(out, "**Query:** {}\n\n", context.query);

    // Entry Points
    out.push_str("### Entry Points\n");
    if context.entry_points.is_empty() {
        out.push_str("_No entry points found._\n\n");
    } else {
        for node in &context.entry_points {
            let _ = writeln!(
                out,
                "- **{}** ({}) - {}:{}",
                node.name,
                node.kind.as_str(),
                node.file_path,
                node.start_line + 1,
            );
            if let Some(ref sig) = node.signature {
                let _ = writeln!(out, "  `{sig}`");
            }
        }
        out.push('\n');
    }

    // Related Symbols grouped by file
    out.push_str("### Related Symbols\n");
    if context.subgraph.nodes.is_empty() {
        out.push_str("_No related symbols._\n\n");
    } else {
        // Group nodes by file_path
        let mut by_file: HashMap<&str, Vec<(&str, u32)>> = HashMap::new();
        for node in &context.subgraph.nodes {
            by_file
                .entry(&node.file_path)
                .or_default()
                .push((&node.name, node.start_line + 1));
        }

        let mut files: Vec<&&str> = by_file.keys().collect();
        files.sort();

        for file in files {
            let symbols = by_file.get(*file).unwrap_or(&Vec::new()).clone();
            let formatted: Vec<String> = symbols
                .iter()
                .map(|(name, line)| format!("{name}:{line}"))
                .collect();
            let _ = writeln!(out, "- {}: {}", file, formatted.join(", "));
        }
        out.push('\n');
    }

    // Code blocks
    out.push_str("### Code\n");
    if context.code_blocks.is_empty() {
        out.push_str("_No code blocks extracted._\n");
    } else {
        for block in &context.code_blocks {
            // Determine a label from the node if available
            let label = if let Some(ref node_id) = block.node_id {
                // Try to find a matching entry point name
                context
                    .entry_points
                    .iter()
                    .find(|n| &n.id == node_id)
                    .map_or_else(|| node_id.clone(), |n| n.name.clone())
            } else {
                "unknown".to_string()
            };

            let _ = writeln!(
                out,
                "#### {} ({}:{})",
                label,
                block.file_path,
                block.start_line + 1,
            );
            // Fence language from the block's file extension, not a hardcoded
            // `rust` (#208).
            let _ = writeln!(out, "```{}", fence_language(&block.file_path));
            out.push_str(&block.content);
            if !block.content.ends_with('\n') {
                out.push('\n');
            }
            out.push_str("```\n\n");
        }
    }

    debug_assert!(
        !out.is_empty(),
        "format_context_as_markdown produced empty output"
    );
    debug_assert!(
        out.contains("## Code Context"),
        "output missing required header"
    );
    out
}

/// Formats a `TaskContext` as pretty-printed JSON.
pub fn format_context_as_json(context: &TaskContext) -> String {
    serde_json::to_string_pretty(context).unwrap_or_default()
}

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
    use super::*;
    use crate::types::*;

    fn make_test_context() -> TaskContext {
        TaskContext {
            query: "test query".to_string(),
            summary: "Test summary".to_string(),
            subgraph: Subgraph::default(),
            entry_points: vec![],
            code_blocks: vec![],
            related_files: vec![],
            seen_node_ids: vec![],
        }
    }

    #[test]
    fn test_markdown_contains_header() {
        let ctx = make_test_context();
        let md = format_context_as_markdown(&ctx);
        assert!(md.contains("## Code Context"));
        assert!(md.contains("test query"));
    }

    #[test]
    fn test_json_roundtrip() {
        let ctx = make_test_context();
        let json = format_context_as_json(&ctx);
        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed["query"], "test query");
    }

    #[test]
    fn test_markdown_with_entry_points() {
        let ctx = TaskContext {
            query: "process".to_string(),
            summary: "Found 1 entry point".to_string(),
            subgraph: Subgraph::default(),
            entry_points: vec![Node {
                id: "function:abc123".to_string(),
                kind: NodeKind::Function,
                name: "process_data".to_string(),
                qualified_name: "src/lib.rs::process_data".to_string(),
                file_path: "src/lib.rs".to_string(),
                start_line: 10,
                attrs_start_line: 10,
                end_line: 20,
                start_column: 0,
                end_column: 1,
                signature: Some("pub fn process_data(input: &str) -> Result<()>".to_string()),
                docstring: None,
                visibility: Visibility::Pub,
                is_async: false,
                branches: 0,
                loops: 0,
                returns: 0,
                max_nesting: 0,
                unsafe_blocks: 0,
                unchecked_calls: 0,
                assertions: 0,
                cognitive_complexity: 0,
                distinct_operators: 0,
                distinct_operands: 0,
                total_operators: 0,
                total_operands: 0,
                updated_at: 0,
                parent_id: None,
            }],
            code_blocks: vec![],
            related_files: vec!["src/lib.rs".to_string()],
            seen_node_ids: vec![],
        };

        let md = format_context_as_markdown(&ctx);
        assert!(md.contains("**process_data**"));
        assert!(md.contains("(function)"));
        assert!(md.contains("src/lib.rs:11"));
        assert!(md.contains("`pub fn process_data(input: &str) -> Result<()>`"));
    }

    #[test]
    fn test_markdown_with_code_blocks() {
        let ctx = TaskContext {
            query: "test".to_string(),
            summary: "Summary".to_string(),
            subgraph: Subgraph::default(),
            entry_points: vec![Node {
                id: "function:abc".to_string(),
                kind: NodeKind::Function,
                name: "my_fn".to_string(),
                qualified_name: "my_fn".to_string(),
                file_path: "src/main.rs".to_string(),
                start_line: 1,
                attrs_start_line: 1,
                end_line: 3,
                start_column: 0,
                end_column: 1,
                signature: None,
                docstring: None,
                visibility: Visibility::Pub,
                is_async: false,
                branches: 0,
                loops: 0,
                returns: 0,
                max_nesting: 0,
                unsafe_blocks: 0,
                unchecked_calls: 0,
                assertions: 0,
                cognitive_complexity: 0,
                distinct_operators: 0,
                distinct_operands: 0,
                total_operators: 0,
                total_operands: 0,
                updated_at: 0,
                parent_id: None,
            }],
            code_blocks: vec![CodeBlock {
                content: "fn my_fn() {\n    println!(\"hello\");\n}".to_string(),
                file_path: "src/main.rs".to_string(),
                start_line: 1,
                end_line: 3,
                node_id: Some("function:abc".to_string()),
            }],
            related_files: vec!["src/main.rs".to_string()],
            seen_node_ids: vec![],
        };

        let md = format_context_as_markdown(&ctx);
        assert!(md.contains("#### my_fn (src/main.rs:2)"));
        assert!(md.contains("```rust"));
        assert!(md.contains("fn my_fn()"));
    }

    #[test]
    fn test_fence_language_from_extension() {
        // #208
        assert_eq!(super::fence_language("a/b.tsx"), "tsx");
        assert_eq!(super::fence_language("a/b.ts"), "typescript");
        assert_eq!(super::fence_language("a/b.rs"), "rust");
        assert_eq!(super::fence_language("a/b.unknownext"), "");
    }
}