1use serde::{Deserialize, Serialize};
14
15use crate::search::SnippetLine;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct PackItem {
20 pub path: String,
21 pub lang: String,
22 pub name: String,
23 pub kind: String,
24 pub line_start: u32,
25 pub line_end: u32,
26 #[serde(skip_serializing_if = "Option::is_none")]
27 pub signature: Option<String>,
28 pub snippet: Vec<SnippetLine>,
29 pub reason: String,
31 pub score: f32,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct ContextPack {
37 pub task: String,
38 pub budget_tokens: u64,
39 pub used_tokens: u64,
40 pub truncated: bool,
42 pub items: Vec<PackItem>,
43}
44
45pub const CHARS_PER_TOKEN: u64 = 4;
47
48pub fn est_tokens(chars: u64) -> u64 {
50 chars / CHARS_PER_TOKEN
51}
52
53pub fn tokenize(task: &str) -> Vec<String> {
57 let mut terms: Vec<String> = Vec::new();
58 let mut seen = std::collections::HashSet::new();
59 for raw in task.split(|c: char| !(c.is_alphanumeric() || c == '_')) {
60 if raw.is_empty() {
61 continue;
62 }
63 for tok in split_identifier(raw) {
64 if tok.len() < 2 || is_stopword(&tok) {
65 continue;
66 }
67 if seen.insert(tok.clone()) {
68 terms.push(tok);
69 }
70 }
71 }
72 terms
73}
74
75fn is_stopword(t: &str) -> bool {
76 matches!(
77 t,
78 "the"
79 | "a"
80 | "an"
81 | "of"
82 | "to"
83 | "in"
84 | "is"
85 | "for"
86 | "and"
87 | "or"
88 | "how"
89 | "where"
90 | "what"
91 | "does"
92 | "do"
93 | "with"
94 | "on"
95 | "by"
96 | "this"
97 | "that"
98 | "it"
99 | "be"
100 | "as"
101 | "at"
102 | "we"
103 | "i"
104 | "add"
105 | "fix"
106 | "use"
107 | "using"
108 | "make"
109 | "get"
110 | "set"
111 | "all"
112 | "when"
113 | "from"
114 | "into"
115 | "via"
116 | "can"
117 | "should"
118 | "code"
119 | "function"
120 | "method"
121 )
122}
123
124pub fn lexical_score(
126 name: &str,
127 kind: &str,
128 signature: Option<&str>,
129 container: Option<&str>,
130 path: &str,
131 terms: &[String],
132) -> f32 {
133 if terms.is_empty() {
134 return 0.0;
135 }
136 let name_lower = name.to_ascii_lowercase();
137 let name_tokens = split_identifier(name);
138 let sig_lower = signature.map(|s| s.to_ascii_lowercase());
139 let cont_tokens = container.map(split_identifier).unwrap_or_default();
140 let path_lower = path.to_ascii_lowercase();
141
142 let mut score = 0.0f32;
143 for term in terms {
144 if name_lower == *term {
145 score += 20.0;
146 } else if name_tokens.iter().any(|t| t == term) {
147 score += 12.0;
148 } else if name_lower.contains(term.as_str()) {
149 score += 6.0;
150 }
151 if cont_tokens.iter().any(|t| t == term) {
152 score += 4.0;
153 }
154 if let Some(sig) = &sig_lower {
155 if sig.contains(term.as_str()) {
156 score += 3.0;
157 }
158 }
159 if path_lower.contains(term.as_str()) {
160 score += 2.0;
161 }
162 }
163 if score > 0.0 && is_priority_kind(kind) {
164 score += 2.0;
165 }
166 score
167}
168
169fn is_priority_kind(kind: &str) -> bool {
170 matches!(
171 kind,
172 "function"
173 | "method"
174 | "struct"
175 | "class"
176 | "trait"
177 | "interface"
178 | "enum"
179 | "type"
180 | "constructor"
181 | "module"
182 )
183}
184
185pub fn split_identifier(s: &str) -> Vec<String> {
187 let mut tokens = Vec::new();
188 let mut cur = String::new();
189 let mut prev_lower = false;
190 for ch in s.chars() {
191 if ch == '_' || ch == '-' || ch == ' ' {
192 if !cur.is_empty() {
193 tokens.push(std::mem::take(&mut cur));
194 }
195 prev_lower = false;
196 continue;
197 }
198 if ch.is_uppercase() && prev_lower && !cur.is_empty() {
199 tokens.push(std::mem::take(&mut cur));
200 }
201 cur.extend(ch.to_lowercase());
202 prev_lower = ch.is_lowercase() || ch.is_numeric();
203 }
204 if !cur.is_empty() {
205 tokens.push(cur);
206 }
207 tokens
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 #[test]
215 fn tokenizes_and_drops_stopwords() {
216 let t = tokenize("How does the SegmentWriter flush to disk?");
217 assert!(t.contains(&"segment".to_string()));
218 assert!(t.contains(&"writer".to_string()));
219 assert!(t.contains(&"flush".to_string()));
220 assert!(t.contains(&"disk".to_string()));
221 assert!(!t.contains(&"the".to_string()));
222 assert!(!t.contains(&"how".to_string()));
223 }
224
225 #[test]
226 fn scores_name_matches_highest() {
227 let terms = tokenize("flush segment writer");
228 let exact = lexical_score("flush", "function", None, None, "src/a.rs", &terms);
229 let unrelated = lexical_score("zebra", "function", None, None, "src/a.rs", &terms);
230 assert!(exact > unrelated);
231 assert_eq!(unrelated, 0.0);
232 }
233}