1use crate::types::*;
3use std::cmp::Ordering;
4
5pub struct BasicCompletionRanker {
7 weights: RankingWeights,
9 history: Option<std::sync::Arc<crate::history::CompletionHistory>>,
11}
12
13impl BasicCompletionRanker {
14 pub fn new(weights: RankingWeights) -> Self {
15 Self {
16 weights,
17 history: None,
18 }
19 }
20
21 pub fn with_history(
22 weights: RankingWeights,
23 history: std::sync::Arc<crate::history::CompletionHistory>,
24 ) -> Self {
25 Self {
26 weights,
27 history: Some(history),
28 }
29 }
30
31 pub fn default_weights() -> Self {
32 Self::new(RankingWeights::default())
33 }
34
35 fn filter_by_prefix(&self, items: Vec<CompletionItem>, prefix: &str) -> Vec<CompletionItem> {
37 if prefix.is_empty() {
38 return items;
39 }
40
41 items
42 .into_iter()
43 .filter(|item| {
44 let filter_text = item.filter_text.as_ref().unwrap_or(&item.label);
45 filter_text
46 .to_lowercase()
47 .starts_with(&prefix.to_lowercase())
48 })
49 .collect()
50 }
51
52 fn fuzzy_match_score(&self, text: &str, pattern: &str) -> f32 {
55 let text_lower = text.to_lowercase();
56 let pattern_lower = pattern.to_lowercase();
57
58 if text_lower == pattern_lower {
60 return 1.0;
61 }
62
63 if text_lower.starts_with(&pattern_lower) {
65 return 0.9;
66 }
67
68 let mut pattern_idx = 0;
70 let mut text_idx = 0;
71 let pattern_chars: Vec<char> = pattern_lower.chars().collect();
72 let text_chars: Vec<char> = text_lower.chars().collect();
73
74 while pattern_idx < pattern_chars.len() && text_idx < text_chars.len() {
75 if pattern_chars[pattern_idx] == text_chars[text_idx] {
76 pattern_idx += 1;
77 }
78 text_idx += 1;
79 }
80
81 if pattern_idx == pattern_chars.len() {
82 let match_density = pattern_chars.len() as f32 / text_chars.len() as f32;
84 0.5 * match_density
85 } else {
86 0.0
87 }
88 }
89
90 #[allow(dead_code)]
92 fn case_insensitive_score(&self, text: &str, pattern: &str) -> f32 {
93 if text.to_lowercase() == pattern.to_lowercase() {
94 1.0
95 } else {
96 0.0
97 }
98 }
99
100 fn calculate_relevance_score(&self, item: &CompletionItem, context: &CompletionContext) -> f32 {
102 let mut score: f32 = 0.5; if let Some(expected_type) = &context.expected_type {
106 if let Some(detail) = &item.detail {
107 if detail.contains(&expected_type.name) {
108 score += 0.3;
109 }
110 }
111 }
112
113 match item.kind {
115 CompletionItemKind::Variable | CompletionItemKind::Function => score += 0.2,
116 CompletionItemKind::Keyword => score += 0.1,
117 _ => {}
118 }
119
120 score.min(1.0)
121 }
122}
123
124impl crate::engine::CompletionRanker for BasicCompletionRanker {
125 fn rank_completions(
126 &self,
127 items: Vec<CompletionItem>,
128 context: &CompletionContext,
129 ) -> Vec<CompletionItem> {
130 let mut filtered = self.filter_by_prefix(items, &context.prefix);
132
133 for item in &mut filtered {
135 let prefix_score = self.fuzzy_match_score(&item.label, &context.prefix);
136 let relevance_score = self.calculate_relevance_score(item, context);
137 let frequency_score = self.score_frequency(item);
138
139 let combined_score = (prefix_score * 0.4)
141 + (relevance_score * self.weights.relevance)
142 + (frequency_score * self.weights.frequency);
143
144 item.score = combined_score.min(1.0);
145 }
146
147 filtered.sort_by(|a, b| match b.score.partial_cmp(&a.score) {
149 Some(Ordering::Equal) | None => a.label.cmp(&b.label),
150 other => other.unwrap(),
151 });
152
153 filtered
154 }
155
156 fn score_relevance(&self, item: &CompletionItem, context: &CompletionContext) -> f32 {
157 self.calculate_relevance_score(item, context)
158 }
159
160 fn score_frequency(&self, item: &CompletionItem) -> f32 {
161 if let Some(history) = &self.history {
162 let frequency_score = history
164 .get_frequency_score(&item.label, "generic")
165 .unwrap_or(0.0);
166 let recency_score = history
167 .get_recency_score(&item.label, "generic")
168 .unwrap_or(0.0);
169
170 (frequency_score * self.weights.frequency + recency_score * self.weights.recency)
172 / (self.weights.frequency + self.weights.recency)
173 } else {
174 0.5
176 }
177 }
178}
179
180pub struct AdvancedCompletionRanker {
182 basic_ranker: BasicCompletionRanker,
183}
184
185impl AdvancedCompletionRanker {
186 pub fn new(weights: RankingWeights) -> Self {
187 Self {
188 basic_ranker: BasicCompletionRanker::new(weights),
189 }
190 }
191
192 pub fn with_history(
193 weights: RankingWeights,
194 history: std::sync::Arc<crate::history::CompletionHistory>,
195 ) -> Self {
196 Self {
197 basic_ranker: BasicCompletionRanker::with_history(weights, history),
198 }
199 }
200
201 pub fn default_weights() -> Self {
202 Self::new(RankingWeights::default())
203 }
204
205 fn fuzzy_filter(&self, items: Vec<CompletionItem>, pattern: &str) -> Vec<CompletionItem> {
207 if pattern.is_empty() {
208 return items;
209 }
210
211 items
212 .into_iter()
213 .filter_map(|mut item| {
214 let filter_text = item.filter_text.as_ref().unwrap_or(&item.label);
215 let score = self.basic_ranker.fuzzy_match_score(filter_text, pattern);
216 if score > 0.0 {
217 item.score = score;
218 Some(item)
219 } else {
220 None
221 }
222 })
223 .collect()
224 }
225}
226
227impl crate::engine::CompletionRanker for AdvancedCompletionRanker {
228 fn rank_completions(
229 &self,
230 items: Vec<CompletionItem>,
231 context: &CompletionContext,
232 ) -> Vec<CompletionItem> {
233 let mut filtered = self.fuzzy_filter(items, &context.prefix);
235
236 for item in &mut filtered {
238 let relevance_score = self.basic_ranker.score_relevance(item, context);
239 let frequency_score = self.basic_ranker.score_frequency(item);
240
241 let combined_score = (item.score * 0.4)
243 + (relevance_score * self.basic_ranker.weights.relevance)
244 + (frequency_score * self.basic_ranker.weights.frequency);
245
246 item.score = combined_score.min(1.0);
247 }
248
249 filtered.sort_by(|a, b| match b.score.partial_cmp(&a.score) {
251 Some(Ordering::Equal) | None => a.label.cmp(&b.label),
252 other => other.unwrap(),
253 });
254
255 filtered
256 }
257
258 fn score_relevance(&self, item: &CompletionItem, context: &CompletionContext) -> f32 {
259 self.basic_ranker.score_relevance(item, context)
260 }
261
262 fn score_frequency(&self, item: &CompletionItem) -> f32 {
263 self.basic_ranker.score_frequency(item)
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270 use crate::engine::CompletionRanker;
271
272 #[test]
273 fn test_prefix_filter_exact_match() {
274 let ranker = BasicCompletionRanker::default_weights();
275 let items = vec![
276 CompletionItem::new(
277 "test".to_string(),
278 CompletionItemKind::Variable,
279 "test".to_string(),
280 ),
281 CompletionItem::new(
282 "testing".to_string(),
283 CompletionItemKind::Variable,
284 "testing".to_string(),
285 ),
286 CompletionItem::new(
287 "other".to_string(),
288 CompletionItemKind::Variable,
289 "other".to_string(),
290 ),
291 ];
292
293 let filtered = ranker.filter_by_prefix(items, "test");
294 assert_eq!(filtered.len(), 2);
295 assert!(filtered.iter().any(|i| i.label == "test"));
296 assert!(filtered.iter().any(|i| i.label == "testing"));
297 }
298
299 #[test]
300 fn test_prefix_filter_case_insensitive() {
301 let ranker = BasicCompletionRanker::default_weights();
302 let items = vec![
303 CompletionItem::new(
304 "Test".to_string(),
305 CompletionItemKind::Variable,
306 "Test".to_string(),
307 ),
308 CompletionItem::new(
309 "TEST".to_string(),
310 CompletionItemKind::Variable,
311 "TEST".to_string(),
312 ),
313 CompletionItem::new(
314 "other".to_string(),
315 CompletionItemKind::Variable,
316 "other".to_string(),
317 ),
318 ];
319
320 let filtered = ranker.filter_by_prefix(items, "test");
321 assert_eq!(filtered.len(), 2);
322 }
323
324 #[test]
325 fn test_prefix_filter_empty_prefix() {
326 let ranker = BasicCompletionRanker::default_weights();
327 let items = vec![
328 CompletionItem::new(
329 "test".to_string(),
330 CompletionItemKind::Variable,
331 "test".to_string(),
332 ),
333 CompletionItem::new(
334 "other".to_string(),
335 CompletionItemKind::Variable,
336 "other".to_string(),
337 ),
338 ];
339
340 let filtered = ranker.filter_by_prefix(items, "");
341 assert_eq!(filtered.len(), 2);
342 }
343
344 #[test]
345 fn test_fuzzy_match_exact() {
346 let ranker = BasicCompletionRanker::default_weights();
347 let score = ranker.fuzzy_match_score("test", "test");
348 assert_eq!(score, 1.0);
349 }
350
351 #[test]
352 fn test_fuzzy_match_prefix() {
353 let ranker = BasicCompletionRanker::default_weights();
354 let score = ranker.fuzzy_match_score("testing", "test");
355 assert!(score > 0.8);
356 }
357
358 #[test]
359 fn test_fuzzy_match_case_insensitive() {
360 let ranker = BasicCompletionRanker::default_weights();
361 let score = ranker.fuzzy_match_score("Test", "test");
362 assert_eq!(score, 1.0);
363 }
364
365 #[test]
366 fn test_fuzzy_match_partial() {
367 let ranker = BasicCompletionRanker::default_weights();
368 let score = ranker.fuzzy_match_score("test_variable", "tv");
369 assert!(score > 0.0);
370 assert!(score < 0.5);
371 }
372
373 #[test]
374 fn test_fuzzy_match_no_match() {
375 let ranker = BasicCompletionRanker::default_weights();
376 let score = ranker.fuzzy_match_score("test", "xyz");
377 assert_eq!(score, 0.0);
378 }
379
380 #[test]
381 fn test_ranking_sorts_by_score() {
382 let ranker = BasicCompletionRanker::default_weights();
383 let items = vec![
384 CompletionItem::new(
385 "test".to_string(),
386 CompletionItemKind::Variable,
387 "test".to_string(),
388 ),
389 CompletionItem::new(
390 "testing".to_string(),
391 CompletionItemKind::Variable,
392 "testing".to_string(),
393 ),
394 ];
395
396 let context =
397 CompletionContext::new("rust".to_string(), Position::new(0, 0), "test".to_string());
398 let ranked = ranker.rank_completions(items, &context);
399
400 assert_eq!(ranked[0].label, "test");
402 assert_eq!(ranked[1].label, "testing");
403 }
404
405 #[test]
406 fn test_ranking_with_prefix() {
407 let ranker = BasicCompletionRanker::default_weights();
408 let items = vec![
409 CompletionItem::new(
410 "test".to_string(),
411 CompletionItemKind::Variable,
412 "test".to_string(),
413 ),
414 CompletionItem::new(
415 "testing".to_string(),
416 CompletionItemKind::Variable,
417 "testing".to_string(),
418 ),
419 CompletionItem::new(
420 "other".to_string(),
421 CompletionItemKind::Variable,
422 "other".to_string(),
423 ),
424 ];
425
426 let context =
427 CompletionContext::new("rust".to_string(), Position::new(0, 0), "test".to_string());
428 let ranked = ranker.rank_completions(items, &context);
429
430 assert_eq!(ranked.len(), 2);
431 assert!(ranked.iter().all(|i| i.label.starts_with("test")));
432 }
433
434 #[test]
435 fn test_advanced_ranker_fuzzy_filter() {
436 let ranker = AdvancedCompletionRanker::default_weights();
437 let items = vec![
438 CompletionItem::new(
439 "test_variable".to_string(),
440 CompletionItemKind::Variable,
441 "test_variable".to_string(),
442 ),
443 CompletionItem::new(
444 "other".to_string(),
445 CompletionItemKind::Variable,
446 "other".to_string(),
447 ),
448 ];
449
450 let filtered = ranker.fuzzy_filter(items, "tv");
451 assert_eq!(filtered.len(), 1);
452 assert_eq!(filtered[0].label, "test_variable");
453 }
454
455 #[test]
456 fn test_relevance_score_with_expected_type() {
457 let ranker = BasicCompletionRanker::default_weights();
458 let mut item = CompletionItem::new(
459 "test".to_string(),
460 CompletionItemKind::Variable,
461 "test".to_string(),
462 );
463 item = item.with_detail("String".to_string());
464
465 let mut context =
466 CompletionContext::new("rust".to_string(), Position::new(0, 0), "".to_string());
467 context.expected_type = Some(Type::new("String".to_string()));
468
469 let score = ranker.score_relevance(&item, &context);
470 assert!(score > 0.5);
471 }
472
473 #[test]
474 fn test_ranking_stability() {
475 let ranker = BasicCompletionRanker::default_weights();
476 let items = vec![
477 CompletionItem::new(
478 "aaa".to_string(),
479 CompletionItemKind::Variable,
480 "aaa".to_string(),
481 ),
482 CompletionItem::new(
483 "aab".to_string(),
484 CompletionItemKind::Variable,
485 "aab".to_string(),
486 ),
487 CompletionItem::new(
488 "aac".to_string(),
489 CompletionItemKind::Variable,
490 "aac".to_string(),
491 ),
492 ];
493
494 let context =
495 CompletionContext::new("rust".to_string(), Position::new(0, 0), "aa".to_string());
496 let ranked = ranker.rank_completions(items, &context);
497
498 assert_eq!(ranked[0].label, "aaa");
500 assert_eq!(ranked[1].label, "aab");
501 assert_eq!(ranked[2].label, "aac");
502 }
503
504 #[test]
505 fn test_frequency_scoring_with_history() {
506 let history = std::sync::Arc::new(crate::history::CompletionHistory::new());
507 let ranker =
508 BasicCompletionRanker::with_history(RankingWeights::default(), history.clone());
509
510 history
512 .record_usage("test".to_string(), "generic".to_string())
513 .unwrap();
514 history
515 .record_usage("test".to_string(), "generic".to_string())
516 .unwrap();
517
518 let item = CompletionItem::new(
519 "test".to_string(),
520 CompletionItemKind::Variable,
521 "test".to_string(),
522 );
523 let score = ranker.score_frequency(&item);
524
525 assert!(score > 0.0);
527 }
528
529 #[test]
530 fn test_frequency_scoring_without_history() {
531 let ranker = BasicCompletionRanker::default_weights();
532 let item = CompletionItem::new(
533 "test".to_string(),
534 CompletionItemKind::Variable,
535 "test".to_string(),
536 );
537 let score = ranker.score_frequency(&item);
538
539 assert_eq!(score, 0.5);
541 }
542
543 #[test]
544 fn test_ranking_with_frequency_boost() {
545 let history = std::sync::Arc::new(crate::history::CompletionHistory::new());
546 let ranker =
547 BasicCompletionRanker::with_history(RankingWeights::default(), history.clone());
548
549 history
551 .record_usage("frequent".to_string(), "generic".to_string())
552 .unwrap();
553 history
554 .record_usage("frequent".to_string(), "generic".to_string())
555 .unwrap();
556
557 let items = vec![
558 CompletionItem::new(
559 "frequent".to_string(),
560 CompletionItemKind::Variable,
561 "frequent".to_string(),
562 ),
563 CompletionItem::new(
564 "rare".to_string(),
565 CompletionItemKind::Variable,
566 "rare".to_string(),
567 ),
568 ];
569
570 let context =
571 CompletionContext::new("rust".to_string(), Position::new(0, 0), "".to_string());
572 let ranked = ranker.rank_completions(items, &context);
573
574 assert_eq!(ranked[0].label, "frequent");
576 }
577}