use crate::types::{Lang, OutlineEntry, OutlineKind};
pub fn outline_language(lang: Lang) -> Option<tree_sitter::Language> {
let lang = match lang {
Lang::Rust => tree_sitter_rust::LANGUAGE,
Lang::TypeScript => tree_sitter_typescript::LANGUAGE_TYPESCRIPT,
Lang::Tsx => tree_sitter_typescript::LANGUAGE_TSX,
Lang::JavaScript => tree_sitter_javascript::LANGUAGE,
Lang::Python => tree_sitter_python::LANGUAGE,
Lang::Scala => tree_sitter_scala::LANGUAGE,
Lang::Go => tree_sitter_go::LANGUAGE,
Lang::Java => tree_sitter_java::LANGUAGE,
Lang::C => tree_sitter_c::LANGUAGE,
Lang::Cpp => tree_sitter_cpp::LANGUAGE,
Lang::Ruby => tree_sitter_ruby::LANGUAGE,
Lang::Php => tree_sitter_php::LANGUAGE_PHP,
Lang::CSharp => tree_sitter_c_sharp::LANGUAGE,
Lang::Swift => tree_sitter_swift::LANGUAGE,
Lang::Kotlin => tree_sitter_kotlin_ng::LANGUAGE,
Lang::Elixir => tree_sitter_elixir::LANGUAGE,
Lang::Dockerfile | Lang::Make => {
return None;
}
};
Some(lang.into())
}
pub(crate) fn walk_top_level(
root: tree_sitter::Node,
lines: &[&str],
lang: Lang,
) -> Vec<OutlineEntry> {
let mut entries = Vec::new();
let mut cursor = root.walk();
for child in root.children(&mut cursor) {
if let Some(entry) = node_to_entry(child, lines, lang, 0) {
entries.push(entry);
}
}
entries
}
fn node_to_entry(
node: tree_sitter::Node,
lines: &[&str],
lang: Lang,
depth: usize,
) -> Option<OutlineEntry> {
let kind_str = node.kind();
let start_line = node.start_position().row as u32 + 1;
let end_line = node.end_position().row as u32 + 1;
if kind_str == "export_statement" {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if let Some(mut entry) = node_to_entry(child, lines, lang, depth) {
entry.start_line = start_line;
entry.end_line = end_line;
return Some(entry);
}
}
}
let (kind, name, signature) = match kind_str {
"function_declaration"
| "function_definition"
| "function_item"
| "method_definition"
| "method_declaration"
| "constructor_declaration"
| "init_declaration"
| "deinit_declaration"
| "protocol_function_declaration" => {
let name = find_child_text(node, "name", lines)
.or_else(|| find_child_text(node, "identifier", lines))
.unwrap_or_else(|| {
if kind_str == "deinit_declaration" {
"deinit".into()
} else {
"<anonymous>".into()
}
});
let sig = extract_signature(node, lines);
(OutlineKind::Function, name, Some(sig))
}
"class_declaration" | "class_definition" => {
let name = find_child_text(node, "name", lines)
.or_else(|| find_child_text(node, "identifier", lines))
.unwrap_or_else(|| "<anonymous>".into());
(OutlineKind::Class, name, None)
}
"struct_item" | "struct_declaration" => {
let name = find_child_text(node, "name", lines).unwrap_or_else(|| "<anonymous>".into());
(OutlineKind::Struct, name, None)
}
"interface_declaration"
| "type_alias_declaration"
| "trait_item"
| "trait_declaration"
| "trait_definition"
| "protocol_declaration" => {
let name = find_child_text(node, "name", lines).unwrap_or_else(|| "<anonymous>".into());
(OutlineKind::Interface, name, None)
}
"type_item" | "type_definition" | "typealias_declaration" => {
let name = find_child_text(node, "name", lines).unwrap_or_else(|| "<anonymous>".into());
(OutlineKind::TypeAlias, name, None)
}
"enum_item" | "enum_declaration" | "enum_definition" => {
let name = find_child_text(node, "name", lines).unwrap_or_else(|| "<anonymous>".into());
(OutlineKind::Enum, name, None)
}
"impl_item" => {
let name = find_child_text(node, "type", lines).unwrap_or_else(|| "<impl>".into());
(OutlineKind::Module, format!("impl {name}"), None)
}
"object_declaration" | "object_definition" => {
let name = find_child_text(node, "name", lines)
.or_else(|| find_child_text(node, "identifier", lines))
.unwrap_or_else(|| "<anonymous>".into());
(OutlineKind::Module, name, None)
}
"const_item" | "const_declaration" | "static_item" => {
let name = find_child_text(node, "name", lines)
.or_else(|| first_identifier_text(node, lines))
.unwrap_or_else(|| "<const>".into());
(OutlineKind::Constant, name, None)
}
"val_definition" => {
let name = first_identifier_text(node, lines).unwrap_or_else(|| "<val>".into());
(OutlineKind::ImmutableVariable, name, None)
}
"lexical_declaration" | "variable_declaration" | "var_definition" => {
let name = first_identifier_text(node, lines).unwrap_or_else(|| "<var>".into());
(OutlineKind::Variable, name, None)
}
"property_declaration" | "protocol_property_declaration" => {
let name = find_child_text(node, "name", lines)
.or_else(|| first_identifier_text(node, lines))
.unwrap_or_else(|| "<property>".into());
let sig = extract_signature(node, lines);
(OutlineKind::Property, name, Some(sig))
}
"import_statement"
| "import_declaration"
| "import"
| "use_declaration"
| "namespace_use_declaration"
| "use_item"
| "using_directive" => {
let text = node_text(node, lines);
(OutlineKind::Import, text, None)
}
"export_statement" => {
let name = node_text(node, lines);
(OutlineKind::Export, name, None)
}
"mod_item"
| "module"
| "namespace_declaration"
| "namespace_definition"
| "file_scoped_namespace_declaration" => {
let name = find_child_text(node, "name", lines).unwrap_or_else(|| "<module>".into());
(OutlineKind::Module, name, None)
}
"call" if lang == Lang::Elixir => {
return elixir_call_to_entry(node, lines, lang, depth);
}
"unary_operator" if lang == Lang::Elixir => {
return elixir_attr_to_entry(node, lines);
}
_ => return None,
};
let is_namespace = matches!(
kind_str,
"namespace_declaration" | "namespace_definition" | "file_scoped_namespace_declaration"
);
let children = if matches!(
kind,
OutlineKind::Class | OutlineKind::Struct | OutlineKind::Module | OutlineKind::Interface
) && depth < 1
{
let child_depth = if is_namespace { depth } else { depth + 1 };
collect_children(node, lines, lang, child_depth)
} else {
Vec::new()
};
let doc = extract_doc(node, lines);
Some(OutlineEntry {
kind,
name,
start_line,
end_line,
signature,
children,
doc,
})
}
fn collect_children(
node: tree_sitter::Node,
lines: &[&str],
lang: Lang,
depth: usize,
) -> Vec<OutlineEntry> {
let mut children = Vec::new();
let mut cursor = node.walk();
let body = node.children(&mut cursor).find(|c| {
let k = c.kind();
k.contains("body") || k.contains("block") || k == "declaration_list"
});
let parent = body.unwrap_or(node);
let mut cursor2 = parent.walk();
for child in parent.children(&mut cursor2) {
if let Some(entry) = node_to_entry(child, lines, lang, depth) {
children.push(entry);
}
}
children
}
fn extract_signature(node: tree_sitter::Node, lines: &[&str]) -> String {
let start_row = node.start_position().row;
if start_row < lines.len() {
let line = lines[start_row].trim();
if let Some(pos) = line.find('{') {
return line[..pos].trim().to_string();
}
if line.ends_with(':') {
if let Some(pos) = line.rfind(':') {
return line[..pos].trim().to_string();
}
}
if let Some(pos) = line.rfind(" do") {
let after = &line[pos + 3..];
if after.is_empty() || after.starts_with('\n') {
return line[..pos].trim().to_string();
}
}
if let Some(pos) = line.find(", do:") {
return line[..pos].trim().to_string();
}
if line.len() > 120 {
format!("{}...", crate::types::truncate_str(line, 117))
} else {
line.to_string()
}
} else {
String::new()
}
}
fn find_child_text(node: tree_sitter::Node, field: &str, lines: &[&str]) -> Option<String> {
node.child_by_field_name(field).map(|n| node_text(n, lines))
}
fn node_text(node: tree_sitter::Node, lines: &[&str]) -> String {
let row = node.start_position().row;
let col_start = node.start_position().column;
let end_row = node.end_position().row;
if row < lines.len() {
if row == end_row {
let col_end = node.end_position().column.min(lines[row].len());
lines[row][col_start..col_end].to_string()
} else {
let text = &lines[row][col_start..];
if text.len() > 80 {
format!("{}...", crate::types::truncate_str(text, 77))
} else {
text.to_string()
}
}
} else {
String::new()
}
}
fn first_identifier_text(node: tree_sitter::Node, lines: &[&str]) -> Option<String> {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
let kind = child.kind();
if kind.contains("identifier") || kind.contains("name") {
let text = node_text(child, lines);
if !text.is_empty() {
return Some(text);
}
}
if kind.contains("declarator") || kind.contains("declaration") {
let mut inner = child.walk();
for grandchild in child.children(&mut inner) {
if grandchild.kind().contains("identifier") {
let text = node_text(grandchild, lines);
if !text.is_empty() {
return Some(text);
}
}
}
}
}
None
}
fn extract_doc(node: tree_sitter::Node, lines: &[&str]) -> Option<String> {
let prev = node.prev_sibling()?;
let kind = prev.kind();
if kind.contains("comment") || kind.contains("doc") {
let text = node_text(prev, lines);
let trimmed = text
.trim_start_matches("///")
.trim_start_matches("//!")
.trim_start_matches("/**")
.trim_start_matches('#')
.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
} else {
None
}
}
const ELIXIR_DEF_KEYWORDS: &[&str] = &[
"def",
"defp",
"defmacro",
"defmacrop",
"defguard",
"defguardp",
"defdelegate",
];
fn elixir_call_to_entry(
node: tree_sitter::Node,
lines: &[&str],
lang: Lang,
depth: usize,
) -> Option<OutlineEntry> {
let target = node.child_by_field_name("target")?;
let keyword = node_text(target, lines);
let start_line = node.start_position().row as u32 + 1;
let end_line = node.end_position().row as u32 + 1;
let (kind, name, signature) = match keyword.as_str() {
"defmodule" => {
let name = elixir_first_arg_text(node, lines)?;
(OutlineKind::Module, name, None)
}
kw if ELIXIR_DEF_KEYWORDS.contains(&kw) => {
let name = elixir_func_name(node, lines)?;
let sig = extract_signature(node, lines);
(OutlineKind::Function, name, Some(sig))
}
"defstruct" | "defexception" => (OutlineKind::Struct, keyword.clone(), None),
"defprotocol" => {
let name = elixir_first_arg_text(node, lines)?;
(OutlineKind::Interface, name, None)
}
"defimpl" => {
let name = elixir_first_arg_text(node, lines)?;
(OutlineKind::Module, format!("impl {name}"), None)
}
"use" | "import" | "alias" | "require" => {
let text = node_text(node, lines);
(OutlineKind::Import, text, None)
}
_ => return None,
};
let children = if matches!(kind, OutlineKind::Module | OutlineKind::Interface) && depth < 1 {
elixir_collect_children(node, lines, lang, depth + 1)
} else {
Vec::new()
};
let doc = elixir_extract_doc(node, lines);
Some(OutlineEntry {
kind,
name,
start_line,
end_line,
signature,
children,
doc,
})
}
fn elixir_attr_to_entry(node: tree_sitter::Node, lines: &[&str]) -> Option<OutlineEntry> {
let operand = node.child_by_field_name("operand")?;
if operand.kind() != "call" {
return None;
}
let target = operand.child_by_field_name("target")?;
let attr_name = node_text(target, lines);
let start_line = node.start_position().row as u32 + 1;
let end_line = node.end_position().row as u32 + 1;
match attr_name.as_str() {
"type" | "typep" | "opaque" => {
let name = elixir_type_name(operand, lines)?;
let sig = node_text(node, lines);
Some(OutlineEntry {
kind: OutlineKind::TypeAlias,
name,
start_line,
end_line,
signature: Some(sig),
children: Vec::new(),
doc: None,
})
}
"callback" | "macrocallback" => {
let name = elixir_callback_name(operand, lines)?;
let sig = node_text(node, lines);
Some(OutlineEntry {
kind: OutlineKind::Function,
name,
start_line,
end_line,
signature: Some(sig),
children: Vec::new(),
doc: None,
})
}
_ => None,
}
}
fn elixir_first_arg_text(node: tree_sitter::Node, lines: &[&str]) -> Option<String> {
let args = super::treesitter::elixir_arguments(node)?;
let mut cursor = args.walk();
for child in args.children(&mut cursor) {
if child.is_named() {
return Some(node_text(child, lines));
}
}
None
}
fn elixir_func_name(node: tree_sitter::Node, lines: &[&str]) -> Option<String> {
let args = super::treesitter::elixir_arguments(node)?;
let mut cursor = args.walk();
for child in args.children(&mut cursor) {
if !child.is_named() {
continue;
}
return super::treesitter::elixir_extract_func_head_name(child, lines);
}
None
}
fn elixir_type_name(call: tree_sitter::Node, lines: &[&str]) -> Option<String> {
let args = super::treesitter::elixir_arguments(call)?;
let mut cursor = args.walk();
for child in args.children(&mut cursor) {
if !child.is_named() {
continue;
}
if child.kind() == "binary_operator" {
if let Some(left) = child.child_by_field_name("left") {
if left.kind() == "call" {
if let Some(target) = left.child_by_field_name("target") {
return Some(node_text(target, lines));
}
}
return Some(node_text(left, lines));
}
}
if child.kind() == "identifier" {
return Some(node_text(child, lines));
}
}
None
}
fn elixir_callback_name(call: tree_sitter::Node, lines: &[&str]) -> Option<String> {
let args = super::treesitter::elixir_arguments(call)?;
let mut cursor = args.walk();
for child in args.children(&mut cursor) {
if !child.is_named() {
continue;
}
if child.kind() == "binary_operator" {
if let Some(left) = child.child_by_field_name("left") {
return super::treesitter::elixir_extract_func_head_name(left, lines);
}
}
return super::treesitter::elixir_extract_func_head_name(child, lines);
}
None
}
fn elixir_collect_children(
node: tree_sitter::Node,
lines: &[&str],
lang: Lang,
depth: usize,
) -> Vec<OutlineEntry> {
let mut children = Vec::new();
let mut cursor = node.walk();
let Some(do_block) = node.children(&mut cursor).find(|c| c.kind() == "do_block") else {
return children;
};
let mut cursor2 = do_block.walk();
for child in do_block.children(&mut cursor2) {
if let Some(entry) = node_to_entry(child, lines, lang, depth) {
children.push(entry);
}
}
children
}
fn elixir_extract_doc(node: tree_sitter::Node, lines: &[&str]) -> Option<String> {
let prev = node.prev_sibling()?;
if prev.kind() != "unary_operator" {
return None;
}
let operand = prev.child_by_field_name("operand")?;
if operand.kind() != "call" {
return None;
}
let target = operand.child_by_field_name("target")?;
let attr = node_text(target, lines);
if attr != "doc" && attr != "moduledoc" {
return None;
}
let args = super::treesitter::elixir_arguments(operand)?;
let mut cursor = args.walk();
for child in args.children(&mut cursor) {
if !child.is_named() {
continue;
}
match child.kind() {
"boolean" => return None,
"string" | "sigil" => {
return elixir_extract_doc_string(child, lines);
}
_ => {}
}
}
None
}
fn elixir_extract_doc_string(node: tree_sitter::Node, lines: &[&str]) -> Option<String> {
let start_row = node.start_position().row;
let end_row = node.end_position().row;
if start_row == end_row {
let mut text = node_text(node, lines);
if text.starts_with('~') && text.len() >= 2 {
text = text[2..].to_string();
}
let trimmed = text.trim_matches('"').trim();
if trimmed.is_empty() {
return None;
}
return Some(trimmed.to_string());
}
for row in (start_row + 1)..end_row {
if row >= lines.len() {
break;
}
let line = lines[row].trim();
if !line.is_empty() && line != "\"\"\"" {
return Some(line.to_string());
}
}
None
}
pub(crate) fn extract_import_source(text: &str, lang: Option<crate::types::Lang>) -> String {
let trimmed = text.trim().trim_end_matches(';');
if lang == Some(crate::types::Lang::Elixir) {
for prefix in &["use ", "import ", "alias ", "require "] {
if let Some(rest) = trimmed.strip_prefix(prefix) {
return rest.split(',').next().unwrap_or(rest).trim().to_string();
}
}
return trimmed.to_string();
}
if let Some(rest) = trimmed.strip_prefix("use ") {
return rest
.split('{')
.next()
.unwrap_or(rest)
.trim()
.trim_end_matches("::")
.to_string();
}
if trimmed.starts_with("import") {
if let Some(from_pos) = trimmed.find("from ") {
let source = &trimmed[from_pos + 5..];
return source
.trim()
.trim_matches(|c| c == '"' || c == '\'' || c == ';')
.to_string();
}
let after = trimmed.strip_prefix("import ").unwrap_or("");
return after
.trim()
.trim_matches(|c| c == '"' || c == '\'' || c == ';')
.to_string();
}
if let Some(rest) = trimmed.strip_prefix("from ") {
return rest.split_whitespace().next().unwrap_or("").to_string();
}
if let Some(rest) = trimmed.strip_prefix("import ") {
return rest.split_whitespace().next().unwrap_or("").to_string();
}
if let Some(rest) = trimmed.strip_prefix("#include") {
return rest.trim().to_string(); }
trimmed
.split_whitespace()
.last()
.unwrap_or(trimmed)
.to_string()
}
pub fn get_outline_entries(content: &str, lang: Lang) -> Vec<OutlineEntry> {
let Some(ts_lang) = outline_language(lang) else {
return Vec::new();
};
let mut parser = tree_sitter::Parser::new();
if parser.set_language(&ts_lang).is_err() {
return Vec::new();
}
let Some(tree) = parser.parse(content, None) else {
return Vec::new();
};
let lines: Vec<&str> = content.lines().collect();
walk_top_level(tree.root_node(), &lines, lang)
}