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