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