Skip to main content

chronicle/ast/
outline.rs

1use crate::error::AstError;
2use crate::schema::LineRange;
3
4/// What kind of semantic unit this is.
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub enum SemanticKind {
7    Function,
8    Method,
9    Struct,
10    Enum,
11    Extension,
12    Const,
13    Static,
14    TypeAlias,
15    Module,
16    Class,
17    Interface,
18    Namespace,
19    Constructor,
20}
21
22impl SemanticKind {
23    pub fn as_str(&self) -> &'static str {
24        match self {
25            SemanticKind::Function => "function",
26            SemanticKind::Method => "method",
27            SemanticKind::Struct => "struct",
28            SemanticKind::Enum => "enum",
29            SemanticKind::Extension => "extension",
30            SemanticKind::Const => "const",
31            SemanticKind::Static => "static",
32            SemanticKind::TypeAlias => "type_alias",
33            SemanticKind::Module => "module",
34            SemanticKind::Class => "class",
35            SemanticKind::Interface => "interface",
36            SemanticKind::Namespace => "namespace",
37            SemanticKind::Constructor => "constructor",
38        }
39    }
40
41    /// Parse a unit_type string into a SemanticKind (for anchor matching).
42    pub fn from_str_loose(s: &str) -> Option<Self> {
43        match s {
44            "function" | "fn" => Some(SemanticKind::Function),
45            "method" => Some(SemanticKind::Method),
46            "struct" => Some(SemanticKind::Struct),
47            "enum" => Some(SemanticKind::Enum),
48            "trait" | "protocol" => Some(SemanticKind::Interface),
49            "impl" | "extension" | "category" => Some(SemanticKind::Extension),
50            "const" => Some(SemanticKind::Const),
51            "static" => Some(SemanticKind::Static),
52            "type_alias" | "type" => Some(SemanticKind::TypeAlias),
53            "module" | "mod" => Some(SemanticKind::Module),
54            "class" => Some(SemanticKind::Class),
55            "interface" => Some(SemanticKind::Interface),
56            "namespace" | "package" => Some(SemanticKind::Namespace),
57            "constructor" | "ctor" => Some(SemanticKind::Constructor),
58            _ => None,
59        }
60    }
61}
62
63/// A semantic unit extracted from source code via tree-sitter.
64#[derive(Debug, Clone)]
65pub struct OutlineEntry {
66    pub kind: SemanticKind,
67    /// Qualified name, e.g. "MyStruct::my_method"
68    pub name: String,
69    /// The function/method signature (if applicable).
70    pub signature: Option<String>,
71    pub lines: LineRange,
72    /// Parent entry name for nested items (e.g. impl block for methods).
73    pub parent: Option<String>,
74}
75
76/// Compute 1-indexed inclusive line range for a tree-sitter node.
77pub(crate) fn node_line_range(node: tree_sitter::Node) -> LineRange {
78    LineRange {
79        start: node.start_position().row as u32 + 1,
80        end: node.end_position().row as u32 + 1,
81    }
82}
83
84/// Extract the signature: text from the start of the node up to (but not including)
85/// the given body delimiter character.
86pub(crate) fn extract_signature_with_delimiter(
87    node: tree_sitter::Node,
88    source: &[u8],
89    delimiter: char,
90) -> String {
91    let full_text = node.utf8_text(source).unwrap_or("");
92    if let Some(pos) = full_text.find(delimiter) {
93        full_text[..pos].trim().to_string()
94    } else {
95        full_text.trim().to_string()
96    }
97}
98
99/// Returns true if this node is an error or missing node and should be skipped.
100pub(crate) fn should_skip_node(node: &tree_sitter::Node) -> bool {
101    node.is_error() || node.is_missing()
102}
103
104/// Extract an outline of semantic units from Rust source code using tree-sitter.
105#[cfg(feature = "lang-rust")]
106pub fn extract_rust_outline(source: &str) -> Result<Vec<OutlineEntry>, AstError> {
107    let mut parser = tree_sitter::Parser::new();
108    parser
109        .set_language(&tree_sitter_rust::LANGUAGE.into())
110        .map_err(|e| AstError::TreeSitter {
111            message: e.to_string(),
112            location: snafu::Location::new(file!(), line!(), 0),
113        })?;
114
115    let tree = parser.parse(source, None).ok_or(AstError::ParseFailed {
116        path: "<input>".to_string(),
117        message: "tree-sitter returned None".to_string(),
118        location: snafu::Location::new(file!(), line!(), 0),
119    })?;
120
121    let mut entries = Vec::new();
122    let root = tree.root_node();
123    let bytes = source.as_bytes();
124
125    walk_rust_node(root, bytes, None, &mut entries);
126
127    Ok(entries)
128}
129
130#[cfg(feature = "lang-rust")]
131fn walk_rust_node(
132    node: tree_sitter::Node,
133    source: &[u8],
134    impl_type_name: Option<&str>,
135    entries: &mut Vec<OutlineEntry>,
136) {
137    let mut cursor = node.walk();
138    for child in node.children(&mut cursor) {
139        match child.kind() {
140            "function_item" => {
141                if let Some(entry) = extract_function(child, source, impl_type_name) {
142                    entries.push(entry);
143                }
144            }
145            "struct_item" => {
146                if let Some(entry) = extract_named_item(child, source, SemanticKind::Struct) {
147                    entries.push(entry);
148                }
149            }
150            "enum_item" => {
151                if let Some(entry) = extract_named_item(child, source, SemanticKind::Enum) {
152                    entries.push(entry);
153                }
154            }
155            "trait_item" => {
156                if let Some(entry) = extract_named_item(child, source, SemanticKind::Interface) {
157                    entries.push(entry);
158                }
159            }
160            "impl_item" => {
161                extract_impl(child, source, entries);
162            }
163            _ => {}
164        }
165    }
166}
167
168#[cfg(feature = "lang-rust")]
169fn extract_function(
170    node: tree_sitter::Node,
171    source: &[u8],
172    impl_type_name: Option<&str>,
173) -> Option<OutlineEntry> {
174    let name_node = node.child_by_field_name("name")?;
175    let fn_name = name_node.utf8_text(source).ok()?;
176
177    let (kind, qualified_name, parent) = if let Some(type_name) = impl_type_name {
178        (
179            SemanticKind::Method,
180            format!("{}::{}", type_name, fn_name),
181            Some(type_name.to_string()),
182        )
183    } else {
184        (SemanticKind::Function, fn_name.to_string(), None)
185    };
186
187    let signature = extract_signature(node, source);
188
189    Some(OutlineEntry {
190        kind,
191        name: qualified_name,
192        signature: Some(signature),
193        lines: LineRange {
194            start: node.start_position().row as u32 + 1,
195            end: node.end_position().row as u32 + 1,
196        },
197        parent,
198    })
199}
200
201#[cfg(feature = "lang-rust")]
202fn extract_named_item(
203    node: tree_sitter::Node,
204    source: &[u8],
205    kind: SemanticKind,
206) -> Option<OutlineEntry> {
207    let name_node = node.child_by_field_name("name")?;
208    let name = name_node.utf8_text(source).ok()?;
209
210    let signature = extract_signature(node, source);
211
212    Some(OutlineEntry {
213        kind,
214        name: name.to_string(),
215        signature: Some(signature),
216        lines: LineRange {
217            start: node.start_position().row as u32 + 1,
218            end: node.end_position().row as u32 + 1,
219        },
220        parent: None,
221    })
222}
223
224#[cfg(feature = "lang-rust")]
225fn extract_impl(node: tree_sitter::Node, source: &[u8], entries: &mut Vec<OutlineEntry>) {
226    // Find the type name for the impl block.
227    // impl blocks have a "type" field for the type being implemented.
228    let type_node = node.child_by_field_name("type");
229    let type_name = type_node
230        .and_then(|n| n.utf8_text(source).ok())
231        .unwrap_or("<unknown>");
232
233    let signature = extract_signature(node, source);
234
235    entries.push(OutlineEntry {
236        kind: SemanticKind::Extension,
237        name: type_name.to_string(),
238        signature: Some(signature),
239        lines: LineRange {
240            start: node.start_position().row as u32 + 1,
241            end: node.end_position().row as u32 + 1,
242        },
243        parent: None,
244    });
245
246    // Descend into the impl body to find methods
247    if let Some(body) = node.child_by_field_name("body") {
248        walk_rust_node(body, source, Some(type_name), entries);
249    }
250}
251
252#[cfg(feature = "lang-rust")]
253fn extract_signature(node: tree_sitter::Node, source: &[u8]) -> String {
254    extract_signature_with_delimiter(node, source, '{')
255}