reovim_plugin_completion/source/
buffer.rs1#![allow(dead_code)] use std::{collections::HashSet, future::Future, pin::Pin};
9
10use reovim_core::completion::{CompletionContext, CompletionItem};
11
12use crate::registry::SourceSupport;
13
14pub struct BufferWordsSource {
16 min_word_length: usize,
18}
19
20impl BufferWordsSource {
21 #[must_use]
23 pub const fn new() -> Self {
24 Self { min_word_length: 2 }
25 }
26
27 #[must_use]
29 pub const fn with_min_word_length(mut self, len: usize) -> Self {
30 self.min_word_length = len;
31 self
32 }
33
34 fn is_word_char(ch: char) -> bool {
36 ch.is_alphanumeric() || ch == '_'
37 }
38
39 fn extract_words(
41 &self,
42 content: &str,
43 current_row: u32,
44 word_start_col: u32,
45 current_col: u32,
46 ) -> Vec<String> {
47 let mut words = HashSet::new();
48
49 for (line_idx, line) in content.lines().enumerate() {
50 let chars: Vec<char> = line.chars().collect();
51 let mut i = 0;
52
53 while i < chars.len() {
54 if !Self::is_word_char(chars[i]) {
56 i += 1;
57 continue;
58 }
59
60 let word_start = i;
62 while i < chars.len() && Self::is_word_char(chars[i]) {
63 i += 1;
64 }
65 let word_end = i;
66
67 let is_current_word = word_start_col < current_col
70 && line_idx == current_row as usize
71 && word_start <= word_start_col as usize
72 && word_end >= current_col as usize;
73
74 if !is_current_word {
75 let word: String = chars[word_start..word_end].iter().collect();
76 if word.len() >= self.min_word_length {
77 words.insert(word);
78 }
79 }
80 }
81 }
82
83 let mut result: Vec<_> = words.into_iter().collect();
84 result.sort();
85 result
86 }
87}
88
89impl Default for BufferWordsSource {
90 fn default() -> Self {
91 Self::new()
92 }
93}
94
95impl SourceSupport for BufferWordsSource {
96 fn source_id(&self) -> &'static str {
97 "buffer_words"
98 }
99
100 fn priority(&self) -> u32 {
101 100 }
103
104 fn complete<'a>(
105 &'a self,
106 ctx: &'a CompletionContext,
107 content: &'a str,
108 ) -> Pin<Box<dyn Future<Output = Vec<CompletionItem>> + Send + 'a>> {
109 Box::pin(async move {
110 let words =
111 self.extract_words(content, ctx.cursor_row, ctx.word_start_col, ctx.cursor_col);
112
113 words
114 .into_iter()
115 .map(|word| CompletionItem::new(word, self.source_id()))
116 .collect()
117 })
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 fn make_context(prefix: &str, row: u32, col: u32, word_start: u32) -> CompletionContext {
126 CompletionContext::new(0, row, col, String::new(), prefix.to_string(), word_start)
127 }
128
129 #[tokio::test]
130 async fn test_extract_words_basic() {
131 let source = BufferWordsSource::new();
132 let content = "hello world foo bar";
133 let ctx = make_context("", 0, 0, 0);
134
135 let items = source.complete(&ctx, content).await;
136 let labels: Vec<_> = items.iter().map(|i| i.label.as_str()).collect();
137
138 assert!(labels.contains(&"hello"));
139 assert!(labels.contains(&"world"));
140 assert!(labels.contains(&"foo"));
141 assert!(labels.contains(&"bar"));
142 }
143
144 #[tokio::test]
145 async fn test_extract_words_multiline() {
146 let source = BufferWordsSource::new();
147 let content = "first line\nsecond line\nthird line";
148 let ctx = make_context("", 0, 0, 0);
149
150 let items = source.complete(&ctx, content).await;
151 let labels: Vec<_> = items.iter().map(|i| i.label.as_str()).collect();
152
153 assert!(labels.contains(&"first"));
154 assert!(labels.contains(&"second"));
155 assert!(labels.contains(&"third"));
156 assert!(labels.contains(&"line"));
157 }
158
159 #[tokio::test]
160 async fn test_excludes_current_word() {
161 let source = BufferWordsSource::new();
162 let content = "hello hel world";
163 let ctx = make_context("hel", 0, 9, 6);
165
166 let items = source.complete(&ctx, content).await;
167 let labels: Vec<_> = items.iter().map(|i| i.label.as_str()).collect();
168
169 assert!(labels.contains(&"hello"));
170 assert!(labels.contains(&"world"));
171 }
173
174 #[tokio::test]
175 async fn test_min_word_length() {
176 let source = BufferWordsSource::new().with_min_word_length(4);
177 let content = "a ab abc abcd abcde";
178 let ctx = make_context("", 0, 0, 0);
179
180 let items = source.complete(&ctx, content).await;
181 let labels: Vec<_> = items.iter().map(|i| i.label.as_str()).collect();
182
183 assert!(!labels.contains(&"a"));
184 assert!(!labels.contains(&"ab"));
185 assert!(!labels.contains(&"abc"));
186 assert!(labels.contains(&"abcd"));
187 assert!(labels.contains(&"abcde"));
188 }
189
190 #[tokio::test]
191 async fn test_deduplicates_words() {
192 let source = BufferWordsSource::new();
193 let content = "hello hello hello world world";
194 let ctx = make_context("", 0, 0, 0);
195
196 let items = source.complete(&ctx, content).await;
197
198 assert_eq!(items.len(), 2);
200 }
201
202 #[tokio::test]
203 async fn test_handles_underscores() {
204 let source = BufferWordsSource::new();
205 let content = "snake_case camelCase _private __dunder__";
206 let ctx = make_context("", 0, 0, 0);
207
208 let items = source.complete(&ctx, content).await;
209 let labels: Vec<_> = items.iter().map(|i| i.label.as_str()).collect();
210
211 assert!(labels.contains(&"snake_case"));
212 assert!(labels.contains(&"camelCase"));
213 assert!(labels.contains(&"_private"));
214 assert!(labels.contains(&"__dunder__"));
215 }
216
217 #[tokio::test]
218 async fn test_handles_numbers() {
219 let source = BufferWordsSource::new();
220 let content = "var1 var2 123 abc123";
221 let ctx = make_context("", 0, 0, 0);
222
223 let items = source.complete(&ctx, content).await;
224 let labels: Vec<_> = items.iter().map(|i| i.label.as_str()).collect();
225
226 assert!(labels.contains(&"var1"));
227 assert!(labels.contains(&"var2"));
228 assert!(labels.contains(&"123"));
229 assert!(labels.contains(&"abc123"));
230 }
231
232 #[tokio::test]
233 async fn test_empty_buffer() {
234 let source = BufferWordsSource::new();
235 let content = "";
236 let ctx = make_context("", 0, 0, 0);
237
238 let items = source.complete(&ctx, content).await;
239 assert!(items.is_empty());
240 }
241
242 #[tokio::test]
243 async fn test_whitespace_only_buffer() {
244 let source = BufferWordsSource::new();
245 let content = " \n\t\n ";
246 let ctx = make_context("", 0, 0, 0);
247
248 let items = source.complete(&ctx, content).await;
249 assert!(items.is_empty());
250 }
251
252 #[tokio::test]
253 async fn test_unicode_words() {
254 let source = BufferWordsSource::new();
255 let content = "日本語 hello 中文 world こんにちは";
256 let ctx = make_context("", 0, 0, 0);
257
258 let items = source.complete(&ctx, content).await;
259 let labels: Vec<_> = items.iter().map(|i| i.label.as_str()).collect();
260
261 assert!(labels.contains(&"日本語"));
262 assert!(labels.contains(&"hello"));
263 assert!(labels.contains(&"中文"));
264 assert!(labels.contains(&"world"));
265 assert!(labels.contains(&"こんにちは"));
266 }
267
268 #[tokio::test]
269 async fn test_unicode_mixed_with_ascii() {
270 let source = BufferWordsSource::new();
271 let content = "变量名1 variable2 函数名_test";
272 let ctx = make_context("", 0, 0, 0);
273
274 let items = source.complete(&ctx, content).await;
275 let labels: Vec<_> = items.iter().map(|i| i.label.as_str()).collect();
276
277 assert!(labels.contains(&"变量名1"));
278 assert!(labels.contains(&"variable2"));
279 assert!(labels.contains(&"函数名_test"));
280 }
281
282 #[tokio::test]
283 async fn test_emoji_handling() {
284 let source = BufferWordsSource::new();
285 let content = "hello🎉world test";
287 let ctx = make_context("", 0, 0, 0);
288
289 let items = source.complete(&ctx, content).await;
290 let labels: Vec<_> = items.iter().map(|i| i.label.as_str()).collect();
291
292 assert!(labels.contains(&"hello"));
294 assert!(labels.contains(&"world"));
295 assert!(labels.contains(&"test"));
296 }
297
298 #[tokio::test]
299 async fn test_very_long_line() {
300 let source = BufferWordsSource::new();
301 let words: Vec<String> = (0..1000).map(|i| format!("word{}", i)).collect();
303 let content = words.join(" ");
304 let ctx = make_context("", 0, 0, 0);
305
306 let items = source.complete(&ctx, content.as_str()).await;
307
308 assert_eq!(items.len(), 1000);
310 }
311
312 #[tokio::test]
313 async fn test_special_characters_as_word_boundaries() {
314 let source = BufferWordsSource::new();
315 let content = "foo.bar baz::qux hello->world test[0]";
316 let ctx = make_context("", 0, 0, 0);
317
318 let items = source.complete(&ctx, content).await;
319 let labels: Vec<_> = items.iter().map(|i| i.label.as_str()).collect();
320
321 assert!(labels.contains(&"foo"));
323 assert!(labels.contains(&"bar"));
324 assert!(labels.contains(&"baz"));
325 assert!(labels.contains(&"qux"));
326 assert!(labels.contains(&"hello"));
327 assert!(labels.contains(&"world"));
328 assert!(labels.contains(&"test"));
329 }
330
331 #[tokio::test]
332 async fn test_single_char_words_filtered() {
333 let source = BufferWordsSource::new(); let content = "a b c ab abc";
335 let ctx = make_context("", 0, 0, 0);
336
337 let items = source.complete(&ctx, content).await;
338 let labels: Vec<_> = items.iter().map(|i| i.label.as_str()).collect();
339
340 assert!(!labels.contains(&"a"));
342 assert!(!labels.contains(&"b"));
343 assert!(!labels.contains(&"c"));
344 assert!(labels.contains(&"ab"));
345 assert!(labels.contains(&"abc"));
346 }
347
348 #[tokio::test]
349 async fn test_cursor_at_beginning_of_word() {
350 let source = BufferWordsSource::new();
351 let content = "hello world";
352 let ctx = make_context("", 0, 0, 0);
354
355 let items = source.complete(&ctx, content).await;
356 let labels: Vec<_> = items.iter().map(|i| i.label.as_str()).collect();
357
358 assert!(labels.contains(&"hello"));
360 assert!(labels.contains(&"world"));
361 }
362
363 #[tokio::test]
364 async fn test_cursor_in_middle_of_word() {
365 let source = BufferWordsSource::new();
366 let content = "hello world testing";
367 let ctx = make_context("wo", 0, 8, 6);
369
370 let items = source.complete(&ctx, content).await;
371 let labels: Vec<_> = items.iter().map(|i| i.label.as_str()).collect();
372
373 assert!(!labels.contains(&"world"));
375 assert!(labels.contains(&"hello"));
376 assert!(labels.contains(&"testing"));
377 }
378
379 #[tokio::test]
380 async fn test_multiline_with_empty_lines() {
381 let source = BufferWordsSource::new();
382 let content = "first\n\nsecond\n\n\nthird";
383 let ctx = make_context("", 0, 0, 0);
384
385 let items = source.complete(&ctx, content).await;
386 let labels: Vec<_> = items.iter().map(|i| i.label.as_str()).collect();
387
388 assert!(labels.contains(&"first"));
389 assert!(labels.contains(&"second"));
390 assert!(labels.contains(&"third"));
391 assert_eq!(items.len(), 3);
392 }
393
394 #[tokio::test]
395 async fn test_tabs_as_word_boundaries() {
396 let source = BufferWordsSource::new();
397 let content = "foo\tbar\tbaz";
398 let ctx = make_context("", 0, 0, 0);
399
400 let items = source.complete(&ctx, content).await;
401 let labels: Vec<_> = items.iter().map(|i| i.label.as_str()).collect();
402
403 assert!(labels.contains(&"foo"));
404 assert!(labels.contains(&"bar"));
405 assert!(labels.contains(&"baz"));
406 }
407
408 #[tokio::test]
409 async fn test_words_with_trailing_numbers() {
410 let source = BufferWordsSource::new();
411 let content = "test1 test2 test3 test123";
412 let ctx = make_context("", 0, 0, 0);
413
414 let items = source.complete(&ctx, content).await;
415 let labels: Vec<_> = items.iter().map(|i| i.label.as_str()).collect();
416
417 assert!(labels.contains(&"test1"));
418 assert!(labels.contains(&"test2"));
419 assert!(labels.contains(&"test3"));
420 assert!(labels.contains(&"test123"));
421 assert_eq!(items.len(), 4);
422 }
423
424 #[tokio::test]
425 async fn test_source_id() {
426 let source = BufferWordsSource::new();
427 assert_eq!(source.source_id(), "buffer_words");
428 }
429
430 #[tokio::test]
431 async fn test_priority() {
432 let source = BufferWordsSource::new();
433 assert_eq!(source.priority(), 100);
434 }
435
436 #[tokio::test]
437 async fn test_completion_item_has_correct_source() {
438 let source = BufferWordsSource::new();
439 let content = "hello world";
440 let ctx = make_context("", 0, 0, 0);
441
442 let items = source.complete(&ctx, content).await;
443
444 for item in &items {
445 assert_eq!(item.source, "buffer_words");
446 }
447 }
448
449 #[tokio::test]
450 async fn test_same_word_different_lines_deduplicated() {
451 let source = BufferWordsSource::new();
452 let content = "function\nfunction\nfunction";
453 let ctx = make_context("", 0, 0, 0);
454
455 let items = source.complete(&ctx, content).await;
456
457 assert_eq!(items.len(), 1);
459 assert_eq!(items[0].label, "function");
460 }
461}