1use ahash::AHashMap;
22
23use crate::symbols::Symbol;
24use crate::types::Chunk;
25
26pub fn chunk_scope_label(symbols: &[Symbol], chunk: &Chunk) -> Option<String> {
40 let same_file = || symbols.iter().filter(|s| s.file_path == chunk.file_path);
41
42 let defined: Vec<&Symbol> = same_file()
43 .filter(|s| s.start_line >= chunk.start_line && s.start_line <= chunk.end_line)
44 .collect();
45 if let Some(first) = defined.first() {
46 return Some(if defined.len() == 1 {
47 format!("defines `{}`", first.name)
48 } else {
49 format!("defines `{}` (+{} more)", first.name, defined.len() - 1)
50 });
51 }
52
53 same_file()
54 .filter(|s| s.start_line < chunk.start_line && chunk.start_line <= s.end_line)
55 .min_by_key(|s| s.end_line.saturating_sub(s.start_line))
56 .map(|s| format!("in `{}`", s.name))
57}
58
59#[derive(Debug, Default)]
72pub struct ScopeIndex {
73 by_file: AHashMap<String, Vec<u32>>,
74}
75
76impl ScopeIndex {
77 pub fn new(symbols: &[Symbol]) -> Self {
79 let mut by_file: AHashMap<String, Vec<u32>> = AHashMap::new();
80 for (i, s) in symbols.iter().enumerate() {
81 by_file
82 .entry(s.file_path.clone())
83 .or_default()
84 .push(i as u32);
85 }
86 Self { by_file }
87 }
88
89 pub fn label(&self, symbols: &[Symbol], chunk: &Chunk) -> Option<String> {
91 let indices = self.by_file.get(chunk.file_path.as_str())?;
92
93 let mut first_defined: Option<&Symbol> = None;
95 let mut defined_count: usize = 0;
96 for &i in indices {
97 let s = symbols.get(i as usize)?;
98 if s.start_line >= chunk.start_line && s.start_line <= chunk.end_line {
99 if first_defined.is_none() {
100 first_defined = Some(s);
101 }
102 defined_count += 1;
103 }
104 }
105 if let Some(first) = first_defined {
106 return Some(if defined_count == 1 {
107 format!("defines `{}`", first.name)
108 } else {
109 format!("defines `{}` (+{} more)", first.name, defined_count - 1)
110 });
111 }
112
113 let mut best: Option<&Symbol> = None;
115 let mut best_span: usize = usize::MAX;
116 for &i in indices {
117 let s = symbols.get(i as usize)?;
118 if s.start_line < chunk.start_line && chunk.start_line <= s.end_line {
119 let span = s.end_line.saturating_sub(s.start_line);
120 if span < best_span {
121 best_span = span;
122 best = Some(s);
123 }
124 }
125 }
126 best.map(|s| format!("in `{}`", s.name))
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use crate::symbols::SymbolKind;
134
135 fn sym(name: &str, kind: SymbolKind, file: &str, start: usize, end: usize) -> Symbol {
136 Symbol {
137 name: name.to_string(),
138 kind,
139 file_path: file.to_string(),
140 start_line: start,
141 end_line: end,
142 language: "rust".to_string(),
143 }
144 }
145
146 fn chunk(file: &str, start: usize, end: usize) -> Chunk {
147 Chunk {
148 content: String::new(),
149 file_path: file.to_string(),
150 start_line: start,
151 end_line: end,
152 language: Some("rust".to_string()),
153 }
154 }
155
156 #[test]
157 fn defines_one_symbol() {
158 let symbols = vec![sym("foo", SymbolKind::Function, "a.rs", 5, 8)];
159 let label = chunk_scope_label(&symbols, &chunk("a.rs", 1, 50));
160 assert_eq!(label.as_deref(), Some("defines `foo`"));
161 }
162
163 #[test]
164 fn defines_with_more() {
165 let symbols = vec![
166 sym("foo", SymbolKind::Function, "a.rs", 5, 8),
167 sym("bar", SymbolKind::Function, "a.rs", 10, 12),
168 sym("baz", SymbolKind::Struct, "a.rs", 14, 20),
169 ];
170 let label = chunk_scope_label(&symbols, &chunk("a.rs", 1, 50));
171 assert_eq!(label.as_deref(), Some("defines `foo` (+2 more)"));
172 }
173
174 #[test]
175 fn picks_innermost_enclosing_when_no_def_inside() {
176 let symbols = vec![
179 sym("outer", SymbolKind::Function, "a.rs", 1, 100),
180 sym("inner", SymbolKind::Function, "a.rs", 30, 60),
181 ];
182 let label = chunk_scope_label(&symbols, &chunk("a.rs", 40, 50));
183 assert_eq!(label.as_deref(), Some("in `inner`"));
184 }
185
186 #[test]
187 fn other_files_ignored() {
188 let symbols = vec![sym("foo", SymbolKind::Function, "b.rs", 5, 8)];
189 let label = chunk_scope_label(&symbols, &chunk("a.rs", 1, 50));
190 assert_eq!(label, None);
191 }
192
193 #[test]
194 fn no_match_returns_none() {
195 let symbols = vec![sym("foo", SymbolKind::Function, "a.rs", 100, 110)];
196 let label = chunk_scope_label(&symbols, &chunk("a.rs", 1, 50));
197 assert_eq!(label, None);
198 }
199
200 #[test]
201 fn scope_index_matches_one_shot() {
202 let symbols = vec![
205 sym("outer", SymbolKind::Function, "a.rs", 1, 100),
206 sym("inner", SymbolKind::Function, "a.rs", 30, 60),
207 sym("other", SymbolKind::Function, "b.rs", 5, 8),
208 ];
209 let idx = ScopeIndex::new(&symbols);
210 let chunks = [
211 chunk("a.rs", 1, 50), chunk("a.rs", 40, 50), chunk("b.rs", 1, 10), chunk("a.rs", 200, 250), chunk("nonexistent.rs", 1, 5),
216 ];
217 for c in &chunks {
218 let one_shot = chunk_scope_label(&symbols, c);
219 let indexed = idx.label(&symbols, c);
220 assert_eq!(
221 one_shot, indexed,
222 "ScopeIndex diverged from chunk_scope_label for chunk {c:?}"
223 );
224 }
225 }
226}