ricecoder_completion/
ranker.rs

1/// Completion ranking and filtering implementation
2use crate::types::*;
3use std::cmp::Ordering;
4
5/// Basic completion ranker with prefix matching and fuzzy matching
6pub struct BasicCompletionRanker {
7    /// Weights for different scoring factors
8    weights: RankingWeights,
9    /// Optional completion history for frequency and recency tracking
10    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    /// Filter completions by prefix match
36    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    /// Calculate fuzzy match score (0.0 to 1.0)
53    /// Returns 1.0 for exact match, decreasing score for fuzzy matches
54    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        // Exact match
59        if text_lower == pattern_lower {
60            return 1.0;
61        }
62
63        // Prefix match
64        if text_lower.starts_with(&pattern_lower) {
65            return 0.9;
66        }
67
68        // Check if all characters of pattern exist in text in order
69        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            // All characters matched, calculate score based on match density
83            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    /// Calculate case-insensitive match score
91    #[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    /// Calculate relevance score based on symbol kind and context
101    fn calculate_relevance_score(&self, item: &CompletionItem, context: &CompletionContext) -> f32 {
102        let mut score: f32 = 0.5; // Base score
103
104        // Boost score for items matching expected type
105        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        // Boost score for items in current scope
114        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        // Filter by prefix
131        let mut filtered = self.filter_by_prefix(items, &context.prefix);
132
133        // Score each item
134        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            // Combine scores using weights
140            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        // Sort by score (descending), then by label (ascending) for stability
148        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            // Get frequency and recency scores from history
163            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            // Combine frequency and recency
171            (frequency_score * self.weights.frequency + recency_score * self.weights.recency)
172                / (self.weights.frequency + self.weights.recency)
173        } else {
174            // Default frequency score when no history is available
175            0.5
176        }
177    }
178}
179
180/// Advanced completion ranker with fuzzy matching and scoring
181pub 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    /// Perform fuzzy matching with scoring
206    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        // Use fuzzy filtering
234        let mut filtered = self.fuzzy_filter(items, &context.prefix);
235
236        // Score each item
237        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            // Combine scores
242            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        // Sort by score (descending), then by label (ascending)
250        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        // Both should match the prefix "test", but "test" is an exact match so should rank higher
401        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        // Should be sorted by label when scores are equal
499        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        // Record some usages
511        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        // Score should be greater than default 0.5 due to frequency
526        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        // Should return default score when no history
540        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        // Record usage for one item
550        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        // Frequent item should rank higher
575        assert_eq!(ranked[0].label, "frequent");
576    }
577}