use anyhow::{Result, bail};
use serde::Serialize;
use serde_json::Value;
use std::path::Path;
use tree_sitter::{Language, Parser, Query, QueryCursor};
use super::args::OutlineArgs;
use super::workspace;
use super::ToolContext;
#[derive(Debug, Clone, Serialize)]
pub(super) struct OutlineItem {
pub kind: String,
pub name: String,
pub line: usize,
}
#[derive(Debug, Serialize)]
pub(super) struct OutlineOutput {
pub path: String,
pub items: Vec<OutlineItem>,
}
pub(super) fn tool_outline(ctx: &mut ToolContext, args: OutlineArgs) -> Result<Value> {
let path = workspace::resolve_read_path(ctx, &args.path)?;
if path.is_dir() {
bail!("outline path is a directory: {}", args.path);
}
let content = workspace::read_file_content(ctx.root(), &path)?;
let lang = detect_language(&path);
let items = match lang {
Some(lang) => parse_outline(&content, lang, args.depth)?,
None => {
Vec::new()
}
};
let output = OutlineOutput {
path: args.path,
items,
};
Ok(serde_json::to_value(output)?)
}
struct LangDef {
language: fn() -> Language,
query: &'static str,
extensions: &'static [&'static str],
}
static LANGUAGES: &[LangDef] = &[
LangDef {
language: || tree_sitter_rust::LANGUAGE.into(),
extensions: &["rs"],
query: r#"
(function_item name: (identifier) @name) @kind
(impl_item type: (type_identifier) @name) @kind
(struct_item name: (type_identifier) @name) @kind
(enum_item name: (type_identifier) @name) @kind
(trait_item name: (type_identifier) @name) @kind
(mod_item name: (identifier) @name) @kind
(const_item name: (identifier) @name) @kind
(static_item name: (identifier) @name) @kind
(type_item name: (type_identifier) @name) @kind
(macro_definition name: (identifier) @name) @kind
"#,
},
LangDef {
language: || tree_sitter_python::LANGUAGE.into(),
extensions: &["py", "pyi"],
query: r#"
(function_definition name: (identifier) @name) @kind
(class_definition name: (identifier) @name) @kind
(decorated_definition definition: (function_definition name: (identifier) @name)) @kind
(decorated_definition definition: (class_definition name: (identifier) @name)) @kind
"#,
},
LangDef {
language: || tree_sitter_javascript::LANGUAGE.into(),
extensions: &["js", "jsx", "mjs", "cjs"],
query: r#"
(function_declaration name: (identifier) @name) @kind
(class_declaration name: (identifier) @name) @kind
(method_definition name: (property_identifier) @name) @kind
(generator_function_declaration name: (identifier) @name) @kind
"#,
},
LangDef {
language: || tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
extensions: &["ts", "mts", "cts"],
query: r#"
(function_declaration name: (identifier) @name) @kind
(class_declaration name: (type_identifier) @name) @kind
(method_definition name: (property_identifier) @name) @kind
(interface_declaration name: (type_identifier) @name) @kind
(type_alias_declaration name: (type_identifier) @name) @kind
(enum_declaration name: (identifier) @name) @kind
"#,
},
LangDef {
language: || tree_sitter_typescript::LANGUAGE_TSX.into(),
extensions: &["tsx"],
query: r#"
(function_declaration name: (identifier) @name) @kind
(class_declaration name: (type_identifier) @name) @kind
(method_definition name: (property_identifier) @name) @kind
(interface_declaration name: (type_identifier) @name) @kind
(type_alias_declaration name: (type_identifier) @name) @kind
(enum_declaration name: (identifier) @name) @kind
"#,
},
LangDef {
language: || tree_sitter_go::LANGUAGE.into(),
extensions: &["go"],
query: r#"
(function_declaration name: (identifier) @name) @kind
(method_declaration name: (field_identifier) @name) @kind
(type_declaration (type_spec name: (type_identifier) @name)) @kind
"#,
},
LangDef {
language: || tree_sitter_java::LANGUAGE.into(),
extensions: &["java"],
query: r#"
(class_declaration name: (identifier) @name) @kind
(interface_declaration name: (identifier) @name) @kind
(method_declaration name: (identifier) @name) @kind
(enum_declaration name: (identifier) @name) @kind
(record_declaration name: (identifier) @name) @kind
"#,
},
LangDef {
language: || tree_sitter_c::LANGUAGE.into(),
extensions: &["c", "h"],
query: r#"
(function_definition declarator: (function_declarator declarator: (identifier) @name)) @kind
(struct_specifier name: (type_identifier) @name) @kind
(enum_specifier name: (type_identifier) @name) @kind
(type_definition declarator: (type_identifier) @name) @kind
"#,
},
LangDef {
language: || tree_sitter_cpp::LANGUAGE.into(),
extensions: &["cpp", "cc", "cxx", "hpp", "hxx", "hh"],
query: r#"
(function_definition declarator: (function_declarator declarator: (identifier) @name)) @kind
(function_definition declarator: (function_declarator declarator: (qualified_identifier name: (identifier) @name))) @kind
(function_definition declarator: (pointer_declarator declarator: (function_declarator declarator: (identifier) @name))) @kind
(struct_specifier name: (type_identifier) @name) @kind
(class_specifier name: (type_identifier) @name) @kind
(enum_specifier name: (type_identifier) @name) @kind
(namespace_definition name: (identifier) @name) @kind
"#,
},
LangDef {
language: || tree_sitter_c_sharp::LANGUAGE.into(),
extensions: &["cs"],
query: r#"
(class_declaration name: (identifier) @name) @kind
(interface_declaration name: (identifier) @name) @kind
(method_declaration name: (identifier) @name) @kind
(struct_declaration name: (identifier) @name) @kind
(enum_declaration name: (identifier) @name) @kind
(record_declaration name: (identifier) @name) @kind
(namespace_declaration name: (identifier) @name) @kind
"#,
},
LangDef {
language: || tree_sitter_ruby::LANGUAGE.into(),
extensions: &["rb"],
query: r#"
(class name: (constant) @name) @kind
(module name: (constant) @name) @kind
(method name: (identifier) @name) @kind
(singleton_method name: (identifier) @name) @kind
"#,
},
LangDef {
language: || tree_sitter_php::LANGUAGE_PHP.into(),
extensions: &["php"],
query: r#"
(function_definition name: (name) @name) @kind
(class_declaration name: (name) @name) @kind
(interface_declaration name: (name) @name) @kind
(method_declaration name: (name) @name) @kind
(trait_declaration name: (name) @name) @kind
(enum_declaration name: (name) @name) @kind
"#,
},
LangDef {
language: || tree_sitter_swift::LANGUAGE.into(),
extensions: &["swift"],
query: r#"
(class_declaration name: (type_identifier) @name) @kind
(struct_declaration name: (type_identifier) @name) @kind
(enum_declaration name: (type_identifier) @name) @kind
(protocol_declaration name: (type_identifier) @name) @kind
(function_declaration name: (simple_identifier) @name) @kind
"#,
},
LangDef {
language: || tree_sitter_kotlin_ng::LANGUAGE.into(),
extensions: &["kt", "kts"],
query: r#"
(class_declaration (simple_identifier) @name) @kind
(function_declaration (simple_identifier) @name) @kind
(object_declaration (simple_identifier) @name) @kind
(interface_declaration (simple_identifier) @name) @kind
"#,
},
LangDef {
language: || tree_sitter_bash::LANGUAGE.into(),
extensions: &["sh", "bash", "zsh"],
query: r#"
(function_definition name: (word) @name) @kind
"#,
},
LangDef {
language: || tree_sitter_lua::LANGUAGE.into(),
extensions: &["lua"],
query: r#"
(function_declaration name: (_) @name) @kind
"#,
},
LangDef {
language: || tree_sitter_dart::LANGUAGE.into(),
extensions: &["dart"],
query: r#"
(class_definition name: (identifier) @name) @kind
(function_signature name: (identifier) @name) @kind
(method_signature name: (identifier) @name) @kind
(enum_declaration name: (identifier) @name) @kind
(mixin_declaration name: (identifier) @name) @kind
(extension_declaration name: (identifier) @name) @kind
"#,
},
];
fn detect_language(path: &Path) -> Option<&'static LangDef> {
let ext = path.extension()?.to_str()?;
LANGUAGES.iter().find(|lang| lang.extensions.contains(&ext))
}
fn node_kind_to_label(ts_kind: &str) -> &'static str {
match ts_kind {
"function_item" => "function",
"impl_item" => "impl",
"struct_item" => "struct",
"enum_item" => "enum",
"trait_item" => "trait",
"mod_item" => "module",
"const_item" => "const",
"static_item" => "static",
"type_item" => "type",
"macro_definition" => "macro",
"function_definition" => "function",
"class_definition" => "class",
"decorated_definition" => "definition",
"function_declaration" => "function",
"class_declaration" => "class",
"method_definition" => "method",
"generator_function_declaration" => "function",
"interface_declaration" => "interface",
"type_alias_declaration" => "type",
"enum_declaration" => "enum",
"method_declaration" => "function",
"type_declaration" => "type",
"type_spec" => "type",
"struct_specifier" => "struct",
"class_specifier" => "class",
"enum_specifier" => "enum",
"namespace_definition" => "namespace",
"struct_declaration" => "struct",
"record_declaration" => "record",
"namespace_declaration" => "namespace",
"class" => "class",
"module" => "module",
"method" => "function",
"singleton_method" => "function",
"trait_declaration" => "trait",
"protocol_declaration" => "protocol",
"object_declaration" => "object",
"function_signature" => "function",
"method_signature" => "method",
"mixin_declaration" => "mixin",
"extension_declaration" => "extension",
_ => {
if ts_kind.contains("function") {
"function"
} else if ts_kind.contains("class") {
"class"
} else if ts_kind.contains("method") {
"method"
} else if ts_kind.contains("struct") {
"struct"
} else if ts_kind.contains("enum") {
"enum"
} else if ts_kind.contains("interface") {
"interface"
} else if ts_kind.contains("trait") {
"trait"
} else if ts_kind.contains("module") || ts_kind.contains("namespace") {
"module"
} else {
"definition"
}
}
}
}
fn parse_outline(source: &str, lang: &LangDef, _depth: usize) -> Result<Vec<OutlineItem>> {
let language = (lang.language)();
let mut parser = Parser::new();
parser
.set_language(&language)
.map_err(|e| anyhow::anyhow!("failed to set language: {e}"))?;
let tree = match parser.parse(source, None) {
Some(tree) => tree,
None => return Ok(Vec::new()),
};
let query = match Query::new(&language, lang.query) {
Ok(q) => q,
Err(_) => {
return Ok(Vec::new());
}
};
let kind_idx = query.capture_index_for_name("kind").unwrap();
let name_idx = query.capture_index_for_name("name").unwrap();
let mut cursor = QueryCursor::new();
let mut matches = cursor.matches(&query, tree.root_node(), source.as_bytes());
let mut items = Vec::new();
let mut seen = std::collections::HashSet::new();
use streaming_iterator::StreamingIterator;
while let Some(m) = matches.next() {
let mut kind_node = None;
let mut name_text = None;
for capture in m.captures {
if capture.index == kind_idx {
kind_node = Some(capture.node);
}
if capture.index == name_idx {
let node = capture.node;
name_text = Some(
source[node.byte_range()]
.lines()
.next()
.unwrap_or("")
.trim()
.to_string(),
);
}
}
if let (Some(kn), Some(name)) = (kind_node, name_text) {
if name.is_empty() || !seen.insert((name.clone(), kn.start_position().row)) {
continue;
}
items.push(OutlineItem {
kind: node_kind_to_label(kn.kind()).to_string(),
name,
line: kn.start_position().row + 1, });
}
}
items.sort_by_key(|item| item.line);
Ok(items)
}