ricecoder_external_lsp/merger/
completion.rs

1//! Completion merging
2
3use crate::types::MergeConfig;
4use ricecoder_completion::types::CompletionItem;
5use std::collections::HashSet;
6
7/// Merges completions from external LSP and internal providers
8pub struct CompletionMerger;
9
10impl CompletionMerger {
11    /// Create a new completion merger
12    pub fn new() -> Self {
13        Self
14    }
15
16    /// Merge completions from external LSP and internal provider
17    ///
18    /// # Arguments
19    ///
20    /// * `external` - Completions from external LSP server (if available)
21    /// * `internal` - Completions from internal provider
22    /// * `config` - Merge configuration
23    ///
24    /// # Returns
25    ///
26    /// Merged and deduplicated completion items
27    pub fn merge(
28        external: Option<Vec<CompletionItem>>,
29        internal: Vec<CompletionItem>,
30        config: &MergeConfig,
31    ) -> Vec<CompletionItem> {
32        let mut result = Vec::new();
33        let mut seen_labels = HashSet::new();
34
35        // Add external completions first (higher priority)
36        if let Some(ext) = external {
37            for item in ext {
38                if config.deduplicate {
39                    if !seen_labels.contains(&item.label) {
40                        seen_labels.insert(item.label.clone());
41                        result.push(item);
42                    }
43                } else {
44                    result.push(item);
45                }
46            }
47        }
48
49        // Add internal completions if configured
50        if config.include_internal {
51            for item in internal {
52                if config.deduplicate {
53                    if !seen_labels.contains(&item.label) {
54                        seen_labels.insert(item.label.clone());
55                        result.push(item);
56                    }
57                } else {
58                    result.push(item);
59                }
60            }
61        }
62
63        // Sort by score (descending)
64        result.sort_by(|a, b| {
65            b.score
66                .partial_cmp(&a.score)
67                .unwrap_or(std::cmp::Ordering::Equal)
68        });
69
70        result
71    }
72}
73
74impl Default for CompletionMerger {
75    fn default() -> Self {
76        Self::new()
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use ricecoder_completion::types::CompletionItemKind;
84
85    fn create_completion(label: &str, score: f32) -> CompletionItem {
86        CompletionItem::new(
87            label.to_string(),
88            CompletionItemKind::Variable,
89            label.to_string(),
90        )
91        .with_score(score)
92    }
93
94    #[test]
95    fn test_merge_external_only() {
96        let external = vec![
97            create_completion("foo", 0.9),
98            create_completion("bar", 0.8),
99        ];
100        let internal = vec![];
101        let config = MergeConfig::default();
102
103        let result = CompletionMerger::merge(Some(external), internal, &config);
104
105        assert_eq!(result.len(), 2);
106        assert_eq!(result[0].label, "foo");
107        assert_eq!(result[1].label, "bar");
108    }
109
110    #[test]
111    fn test_merge_internal_only() {
112        let external = None;
113        let internal = vec![
114            create_completion("baz", 0.7),
115            create_completion("qux", 0.6),
116        ];
117        let config = MergeConfig::default();
118
119        let result = CompletionMerger::merge(external, internal, &config);
120
121        assert_eq!(result.len(), 2);
122        assert_eq!(result[0].label, "baz");
123        assert_eq!(result[1].label, "qux");
124    }
125
126    #[test]
127    fn test_merge_both_with_deduplication() {
128        let external = vec![
129            create_completion("foo", 0.9),
130            create_completion("bar", 0.8),
131        ];
132        let internal = vec![
133            create_completion("foo", 0.5), // Duplicate, should be skipped
134            create_completion("baz", 0.7),
135        ];
136        let config = MergeConfig {
137            include_internal: true,
138            deduplicate: true,
139        };
140
141        let result = CompletionMerger::merge(Some(external), internal, &config);
142
143        assert_eq!(result.len(), 3);
144        // Results should be sorted by score (descending)
145        assert_eq!(result[0].label, "foo"); // 0.9
146        assert_eq!(result[1].label, "bar"); // 0.8
147        assert_eq!(result[2].label, "baz"); // 0.7
148    }
149
150    #[test]
151    fn test_merge_both_without_deduplication() {
152        let external = vec![create_completion("foo", 0.9)];
153        let internal = vec![create_completion("foo", 0.5)];
154        let config = MergeConfig {
155            include_internal: true,
156            deduplicate: false,
157        };
158
159        let result = CompletionMerger::merge(Some(external), internal, &config);
160
161        assert_eq!(result.len(), 2);
162        assert_eq!(result[0].label, "foo");
163        assert_eq!(result[1].label, "foo");
164    }
165
166    #[test]
167    fn test_merge_without_internal() {
168        let external = vec![create_completion("foo", 0.9)];
169        let internal = vec![create_completion("bar", 0.8)];
170        let config = MergeConfig {
171            include_internal: false,
172            deduplicate: true,
173        };
174
175        let result = CompletionMerger::merge(Some(external), internal, &config);
176
177        assert_eq!(result.len(), 1);
178        assert_eq!(result[0].label, "foo");
179    }
180
181    #[test]
182    fn test_merge_sorting_by_score() {
183        let external = vec![
184            create_completion("low", 0.3),
185            create_completion("high", 0.9),
186            create_completion("mid", 0.6),
187        ];
188        let internal = vec![];
189        let config = MergeConfig::default();
190
191        let result = CompletionMerger::merge(Some(external), internal, &config);
192
193        assert_eq!(result.len(), 3);
194        assert_eq!(result[0].label, "high");
195        assert_eq!(result[1].label, "mid");
196        assert_eq!(result[2].label, "low");
197    }
198
199    #[test]
200    fn test_merge_empty_external() {
201        let external = Some(vec![]);
202        let internal = vec![create_completion("foo", 0.8)];
203        let config = MergeConfig::default();
204
205        let result = CompletionMerger::merge(external, internal, &config);
206
207        assert_eq!(result.len(), 1);
208        assert_eq!(result[0].label, "foo");
209    }
210}