mod analyzer;
mod cpp;
mod java;
mod javascript;
mod kotlin;
mod markdown;
mod model;
mod objc;
mod render;
mod rust;
mod swift;
mod tree;
mod util;
pub use analyzer::{analyze_file, analyze_source, detect_language};
pub use model::{Language, Node, RenderOptions, StructureView};
pub use render::render_text;
pub use util::truncate_preview;
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn detects_language_from_extension() {
assert_eq!(
detect_language(Path::new("README.md"), ""),
Language::Markdown
);
assert_eq!(
detect_language(Path::new("src/app.js"), ""),
Language::JavaScript
);
assert_eq!(
detect_language(Path::new("src/MainActivity.java"), ""),
Language::Java
);
assert_eq!(
detect_language(Path::new("src/MainActivity.kt"), ""),
Language::Kotlin
);
assert_eq!(
detect_language(Path::new("build.gradle.kts"), ""),
Language::Kotlin
);
assert_eq!(detect_language(Path::new("src/client.c"), ""), Language::C);
assert_eq!(
detect_language(Path::new("include/client.h"), ""),
Language::C
);
assert_eq!(
detect_language(Path::new("src/client.cpp"), ""),
Language::Cpp
);
assert_eq!(
detect_language(Path::new("include/client.hpp"), ""),
Language::Cpp
);
assert_eq!(
detect_language(Path::new("Sources/App/Client.swift"), ""),
Language::Swift
);
assert_eq!(
detect_language(Path::new("Sources/Client.m"), ""),
Language::ObjectiveC
);
assert_eq!(
detect_language(Path::new("Sources/Client.mm"), ""),
Language::ObjectiveC
);
assert_eq!(
detect_language(
Path::new("Sources/Client.h"),
"#import <Foundation/Foundation.h>"
),
Language::ObjectiveC
);
assert_eq!(
detect_language(Path::new("src/app.ts"), ""),
Language::TypeScript
);
assert_eq!(detect_language(Path::new("src/App.tsx"), ""), Language::Tsx);
assert_eq!(detect_language(Path::new("src/lib.rs"), ""), Language::Rust);
assert_eq!(detect_language(Path::new("LICENSE"), ""), Language::Unknown);
}
#[test]
fn creates_file_node_for_non_empty_source() {
let view = analyze_source("example.txt", Language::Unknown, "\nhello\nworld\n", 20);
assert_eq!(view.nodes.len(), 1);
assert_eq!(view.nodes[0].kind, "file");
assert_eq!(view.nodes[0].start_line, 1);
assert_eq!(view.nodes[0].end_line, 3);
assert_eq!(view.nodes[0].preview.as_deref(), Some("hello"));
}
#[test]
fn serializes_language_labels() {
assert_eq!(
serde_json::to_string(&Language::JavaScript).unwrap(),
"\"javascript\""
);
assert_eq!(serde_json::to_string(&Language::Java).unwrap(), "\"java\"");
assert_eq!(
serde_json::to_string(&Language::Kotlin).unwrap(),
"\"kotlin\""
);
assert_eq!(serde_json::to_string(&Language::C).unwrap(), "\"c\"");
assert_eq!(serde_json::to_string(&Language::Cpp).unwrap(), "\"cpp\"");
assert_eq!(
serde_json::to_string(&Language::Swift).unwrap(),
"\"swift\""
);
assert_eq!(
serde_json::to_string(&Language::ObjectiveC).unwrap(),
"\"objective_c\""
);
assert_eq!(
serde_json::to_string(&Language::TypeScript).unwrap(),
"\"typescript\""
);
assert_eq!(
serde_json::to_string(&Language::Tsx).unwrap(),
"\"typescript\""
);
}
#[test]
fn truncates_preview_on_character_boundary() {
assert_eq!(truncate_preview("abcdef", 4), "abc…");
assert_eq!(truncate_preview("ab", 4), "ab");
}
#[test]
fn extracts_markdown_sections() {
let source = "---\ntitle: Test\n---\n# Intro\n\n- one\n- two\n\n## Details\n\n```rust\nfn main() {}\n```\n# Next\n";
let view = analyze_source("README.md", Language::Markdown, source, 40);
assert_eq!(view.nodes[0].kind, "frontmatter");
assert_eq!(view.nodes[0].start_line, 1);
assert_eq!(view.nodes[0].end_line, 3);
let intro = &view.nodes[1];
assert_eq!(intro.kind, "heading");
assert_eq!(intro.name.as_deref(), Some("Intro"));
assert_eq!(intro.start_line, 4);
assert_eq!(intro.end_line, 13);
assert_eq!(intro.children[0].kind, "list");
assert_eq!(intro.children[1].name.as_deref(), Some("Details"));
assert_eq!(intro.children[1].children[0].kind, "code_block");
assert_eq!(view.nodes[2].name.as_deref(), Some("Next"));
}
#[test]
fn renders_text_as_tree_outline() {
let source = "# Intro\n\n## Details\n";
let view = analyze_source("README.md", Language::Markdown, source, 40);
let output = render_text(
&view,
&RenderOptions {
max_depth: None,
max_nodes: 20,
},
);
assert_eq!(
output,
"README.md (markdown)\n└─ heading Intro L1-3\n └─ heading Details L3-3\n"
);
}
#[test]
fn extracts_rust_items() {
let source = "pub mod api {\n pub struct Client;\n\n impl Client {\n pub fn new() -> Self {\n Self\n }\n }\n}\n\n#[cfg(test)]\nmod tests {\n #[test]\n fn creates_client() {}\n}\n";
let view = analyze_source("src/lib.rs", Language::Rust, source, 80);
let api = &view.nodes[0];
assert_eq!(api.kind, "module");
assert_eq!(api.name.as_deref(), Some("api"));
assert_eq!(api.start_line, 1);
assert_eq!(api.end_line, 9);
assert_eq!(api.children[0].kind, "struct");
assert_eq!(api.children[1].kind, "impl");
assert_eq!(api.children[1].children[0].kind, "function");
assert_eq!(api.children[1].children[0].name.as_deref(), Some("new"));
assert_eq!(api.children[1].children[0].end_line, 7);
let tests = &view.nodes[1];
assert_eq!(tests.name.as_deref(), Some("tests"));
assert_eq!(tests.children[0].kind, "test");
assert_eq!(tests.children[0].name.as_deref(), Some("creates_client"));
}
#[test]
fn tracks_multiline_rust_signatures() {
let source = "pub fn build(\n input: String,\n) -> String {\n input\n}\n";
let view = analyze_source("src/lib.rs", Language::Rust, source, 80);
assert_eq!(view.nodes[0].kind, "function");
assert_eq!(view.nodes[0].name.as_deref(), Some("build"));
assert_eq!(view.nodes[0].start_line, 1);
assert_eq!(view.nodes[0].end_line, 5);
}
#[test]
fn braces_in_char_literals_do_not_hide_closing_blocks() {
let source = "fn contains_quote() {\n let quote = '\"';\n}\nfn next() {}\n";
let view = analyze_source("src/lib.rs", Language::Rust, source, 80);
assert_eq!(view.nodes[0].name.as_deref(), Some("contains_quote"));
assert_eq!(view.nodes[0].end_line, 3);
assert_eq!(view.nodes[1].name.as_deref(), Some("next"));
}
#[test]
fn extracts_rust_ast_fixture_shapes() {
let source = "macro_rules! make_client { () => {}; }\n\n#[derive(Debug)]\npub struct Client {\n value: String,\n}\n\npub enum Mode { Fast, Slow }\n\npub trait Service {\n fn call(&self);\n}\n\nimpl Client {\n pub async fn build(\n value: String,\n ) -> Self {\n make_client!();\n Self { value }\n }\n}\n\nmod tests {\n #[tokio::test(flavor = \"current_thread\")]\n async fn builds_client() {}\n}\n";
let view = analyze_source("src/lib.rs", Language::Rust, source, 80);
assert_eq!(view.nodes[0].kind, "struct");
assert_eq!(view.nodes[0].name.as_deref(), Some("Client"));
assert_eq!(view.nodes[0].start_line, 4);
assert_eq!(view.nodes[0].end_line, 6);
assert_eq!(view.nodes[1].kind, "enum");
assert_eq!(view.nodes[1].name.as_deref(), Some("Mode"));
assert_eq!(view.nodes[2].kind, "trait");
assert_eq!(view.nodes[2].name.as_deref(), Some("Service"));
let impl_node = &view.nodes[3];
assert_eq!(impl_node.kind, "impl");
assert_eq!(impl_node.name.as_deref(), Some("impl Client"));
assert_eq!(impl_node.start_line, 14);
assert_eq!(impl_node.end_line, 21);
assert_eq!(impl_node.children[0].kind, "function");
assert_eq!(impl_node.children[0].name.as_deref(), Some("build"));
assert_eq!(impl_node.children[0].start_line, 15);
assert_eq!(impl_node.children[0].end_line, 20);
let tests = &view.nodes[4];
assert_eq!(tests.kind, "module");
assert_eq!(tests.name.as_deref(), Some("tests"));
assert_eq!(tests.children[0].kind, "test");
assert_eq!(tests.children[0].name.as_deref(), Some("builds_client"));
}
#[test]
fn extracts_javascript_items() {
let source = "export function loadUser(id) {\n return id;\n}\n\nexport class Client {\n fetch() {\n return loadUser(1);\n }\n}\n\nconst helper = () => true;\n";
let view = analyze_source("src/app.js", Language::JavaScript, source, 80);
assert_eq!(view.nodes[0].kind, "function");
assert_eq!(view.nodes[0].name.as_deref(), Some("loadUser"));
assert_eq!(view.nodes[0].start_line, 1);
assert_eq!(view.nodes[0].end_line, 3);
let class = &view.nodes[1];
assert_eq!(class.kind, "class");
assert_eq!(class.name.as_deref(), Some("Client"));
assert_eq!(class.children[0].kind, "method");
assert_eq!(class.children[0].name.as_deref(), Some("fetch"));
assert_eq!(view.nodes[2].kind, "function");
assert_eq!(view.nodes[2].name.as_deref(), Some("helper"));
}
#[test]
fn extracts_java_items() {
let source = "package com.example.app;\n\nimport android.app.Activity;\n\npublic class MainActivity extends Activity {\n private String title;\n\n public MainActivity() {}\n\n @Override\n protected void onCreate(Bundle state) {}\n\n static class Helper {\n void bind() {}\n }\n}\n";
let view = analyze_source(
"app/src/main/java/MainActivity.java",
Language::Java,
source,
80,
);
assert_eq!(view.nodes[0].kind, "package");
assert_eq!(view.nodes[0].name.as_deref(), Some("com.example.app"));
assert_eq!(view.nodes[1].kind, "import");
assert_eq!(view.nodes[1].name.as_deref(), Some("android.app.Activity"));
let class = &view.nodes[2];
assert_eq!(class.kind, "class");
assert_eq!(class.name.as_deref(), Some("MainActivity"));
assert_eq!(class.children[0].kind, "field");
assert_eq!(class.children[0].name.as_deref(), Some("title"));
assert_eq!(class.children[1].kind, "constructor");
assert_eq!(class.children[1].name.as_deref(), Some("MainActivity"));
assert_eq!(class.children[2].kind, "method");
assert_eq!(class.children[2].name.as_deref(), Some("onCreate"));
assert_eq!(class.children[3].kind, "class");
assert_eq!(class.children[3].name.as_deref(), Some("Helper"));
assert_eq!(class.children[3].children[0].kind, "method");
assert_eq!(class.children[3].children[0].name.as_deref(), Some("bind"));
}
#[test]
fn extracts_typescript_items() {
let source = "export interface User {\n id: string;\n}\n\ntype UserId = string;\n\nenum Mode { Fast, Slow }\n\nexport class Service {\n async load(id: UserId): Promise<User> {\n return { id };\n }\n}\n";
let view = analyze_source("src/app.ts", Language::TypeScript, source, 80);
assert_eq!(view.nodes[0].kind, "interface");
assert_eq!(view.nodes[0].name.as_deref(), Some("User"));
assert_eq!(view.nodes[1].kind, "type");
assert_eq!(view.nodes[1].name.as_deref(), Some("UserId"));
assert_eq!(view.nodes[2].kind, "enum");
assert_eq!(view.nodes[2].name.as_deref(), Some("Mode"));
assert_eq!(view.nodes[3].kind, "class");
assert_eq!(view.nodes[3].children[0].kind, "method");
assert_eq!(view.nodes[3].children[0].name.as_deref(), Some("load"));
}
#[test]
fn parses_tsx_function_components() {
let source = "export function App() {\n return <main>Hello</main>;\n}\n";
let view = analyze_source("src/App.tsx", Language::Tsx, source, 80);
assert_eq!(view.nodes[0].kind, "function");
assert_eq!(view.nodes[0].name.as_deref(), Some("App"));
}
}