diffler_core/syntax/
symbols.rs1use std::ops::Range;
5
6use crate::syntax::registry::LanguageRegistry;
7use crate::syntax::scope::tag_pass;
8use crate::syntax::{MAX_PARSE_BYTES, ScopeIndex, parse};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct Ident {
14 pub line: usize,
15 pub range: Range<usize>,
16 pub name: String,
17 pub item: bool,
20}
21
22impl LanguageRegistry {
23 pub fn symbols(&self, path: &str, content: &str) -> (Vec<Ident>, ScopeIndex) {
28 let entry = (content.len() <= MAX_PARSE_BYTES)
29 .then(|| self.for_path(path))
30 .flatten();
31 let Some((entry, tree)) = entry.and_then(|entry| Some((entry, parse(entry, content)?)))
32 else {
33 return (words(content), ScopeIndex::default());
34 };
35 let (scope, tagged) = entry
36 .tags()
37 .map(|query| tag_pass(query, &tree, content))
38 .unwrap_or_default();
39 let mut idents = Vec::new();
40 let mut cursor = tree.walk();
41 loop {
42 let node = cursor.node();
43 if node.child_count() == 0 && !node.is_missing() && is_identifier(node.kind()) {
44 let start = node.start_position();
45 let len = node.end_byte() - node.start_byte();
46 if let Some(name) = content
47 .get(node.start_byte()..node.end_byte())
48 .filter(|name| !name.is_empty())
49 {
50 idents.push(Ident {
51 line: start.row,
52 range: start.column..start.column + len,
53 name: name.to_owned(),
54 item: node.kind().contains("type") || tagged.contains(&node.start_byte()),
55 });
56 }
57 }
58 if cursor.goto_first_child() || cursor.goto_next_sibling() {
59 continue;
60 }
61 loop {
62 if !cursor.goto_parent() {
63 return (idents, scope);
64 }
65 if cursor.goto_next_sibling() {
66 break;
67 }
68 }
69 }
70 }
71}
72
73fn is_identifier(kind: &str) -> bool {
77 kind.ends_with("identifier") || kind == "name" || kind == "constant"
78}
79
80fn words(content: &str) -> Vec<Ident> {
82 let mut out = Vec::new();
83 for (line, text) in content.lines().enumerate() {
84 let mut start = None;
85 for (at, ch) in text
86 .char_indices()
87 .chain(std::iter::once((text.len(), ' ')))
88 {
89 let word_char = ch.is_ascii_alphanumeric() || ch == '_';
90 match (start, word_char) {
91 (None, true) => start = Some(at),
92 (Some(from), false) => {
93 start = None;
94 let Some(token) = text.get(from..at) else {
95 continue;
96 };
97 if !token.starts_with(|first: char| first.is_ascii_digit()) {
98 out.push(Ident {
99 line,
100 range: from..at,
101 name: token.to_owned(),
102 item: false,
103 });
104 }
105 }
106 _ => {}
107 }
108 }
109 }
110 out
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 fn names_on(idents: &[Ident], line: usize) -> Vec<&str> {
118 idents
119 .iter()
120 .filter(|ident| ident.line == line)
121 .map(|ident| ident.name.as_str())
122 .collect()
123 }
124
125 #[test]
126 fn a_parsed_file_yields_its_names_and_skips_strings_and_comments() {
127 let reg = LanguageRegistry::build();
128 let src = "fn total(price: u32, qty: u32) -> u32 {\n // price times qty\n let label = \"qty\";\n price * qty\n}\n";
129 let idents = reg.symbols("a.rs", src).0;
130 assert_eq!(
131 names_on(&idents, 0),
132 ["total", "price", "qty"],
133 "the grammar parses a primitive type as its own node"
134 );
135 assert!(names_on(&idents, 1).is_empty(), "a comment names nothing");
136 assert_eq!(names_on(&idents, 2), ["label"], "a string names nothing");
137 assert_eq!(names_on(&idents, 3), ["price", "qty"]);
138 let price = idents.iter().find(|i| i.line == 3).expect("price");
139 assert_eq!(
140 &src.lines().nth(3).expect("line")[price.range.clone()],
141 "price"
142 );
143 }
144
145 #[test]
146 fn a_call_or_a_definition_names_an_item_and_a_value_does_not() {
147 let reg = LanguageRegistry::build();
148 let src = "fn apply(lens: Lens) -> u32 {\n lens.marks(total)\n}\n";
149 let idents = reg.symbols("a.rs", src).0;
150 let item = |line: usize, name: &str| {
151 idents
152 .iter()
153 .find(|ident| ident.line == line && ident.name == name)
154 .is_some_and(|ident| ident.item)
155 };
156 assert!(item(0, "apply"), "a definition");
157 assert!(item(0, "Lens"), "a type position");
158 assert!(item(1, "marks"), "a method call");
159 assert!(!item(1, "lens"), "the value it is called on");
160 assert!(!item(1, "total"), "an argument");
161 }
162
163 #[test]
164 fn a_file_without_a_grammar_falls_back_to_words() {
165 let reg = LanguageRegistry::build();
166 let idents = reg.symbols("notes.zzz", "total = price * 2qty\n").0;
167 assert_eq!(names_on(&idents, 0), ["total", "price"]);
168 }
169}