rubydex 0.1.0.beta11 → 0.1.0.beta13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +23 -23
  3. data/README.md +125 -125
  4. data/THIRD_PARTY_LICENSES.html +2018 -945
  5. data/exe/rdx +47 -47
  6. data/ext/rubydex/declaration.c +453 -388
  7. data/ext/rubydex/declaration.h +23 -23
  8. data/ext/rubydex/definition.c +284 -197
  9. data/ext/rubydex/definition.h +28 -28
  10. data/ext/rubydex/diagnostic.c +6 -6
  11. data/ext/rubydex/diagnostic.h +11 -11
  12. data/ext/rubydex/document.c +97 -98
  13. data/ext/rubydex/document.h +10 -10
  14. data/ext/rubydex/extconf.rb +146 -127
  15. data/ext/rubydex/graph.c +701 -512
  16. data/ext/rubydex/graph.h +10 -10
  17. data/ext/rubydex/handle.h +44 -44
  18. data/ext/rubydex/location.c +22 -22
  19. data/ext/rubydex/location.h +15 -15
  20. data/ext/rubydex/reference.c +123 -104
  21. data/ext/rubydex/reference.h +15 -16
  22. data/ext/rubydex/rubydex.c +22 -22
  23. data/ext/rubydex/utils.c +108 -86
  24. data/ext/rubydex/utils.h +34 -28
  25. data/lib/rubydex/comment.rb +17 -17
  26. data/lib/rubydex/declaration.rb +11 -0
  27. data/lib/rubydex/diagnostic.rb +21 -21
  28. data/lib/rubydex/failures.rb +15 -15
  29. data/lib/rubydex/graph.rb +98 -92
  30. data/lib/rubydex/keyword.rb +17 -0
  31. data/lib/rubydex/keyword_parameter.rb +13 -0
  32. data/lib/rubydex/location.rb +90 -90
  33. data/lib/rubydex/mixin.rb +22 -0
  34. data/lib/rubydex/version.rb +5 -5
  35. data/lib/rubydex.rb +24 -20
  36. data/rbi/rubydex.rbi +425 -310
  37. data/rust/Cargo.lock +1851 -1851
  38. data/rust/Cargo.toml +29 -29
  39. data/rust/about.toml +10 -10
  40. data/rust/{about.hbs → about_templates/about.hbs} +81 -78
  41. data/rust/about_templates/mingw_licenses.hbs +1071 -0
  42. data/rust/rubydex/Cargo.toml +42 -42
  43. data/rust/rubydex/src/compile_assertions.rs +13 -13
  44. data/rust/rubydex/src/diagnostic.rs +110 -109
  45. data/rust/rubydex/src/errors.rs +28 -28
  46. data/rust/rubydex/src/indexing/local_graph.rs +224 -224
  47. data/rust/rubydex/src/indexing/rbs_indexer.rs +1551 -1554
  48. data/rust/rubydex/src/indexing/ruby_indexer.rs +2329 -6753
  49. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +4962 -0
  50. data/rust/rubydex/src/indexing.rs +210 -210
  51. data/rust/rubydex/src/integrity.rs +279 -278
  52. data/rust/rubydex/src/job_queue.rs +199 -205
  53. data/rust/rubydex/src/lib.rs +17 -17
  54. data/rust/rubydex/src/listing.rs +371 -272
  55. data/rust/rubydex/src/main.rs +160 -160
  56. data/rust/rubydex/src/model/built_in.rs +83 -0
  57. data/rust/rubydex/src/model/comment.rs +24 -24
  58. data/rust/rubydex/src/model/declaration.rs +679 -588
  59. data/rust/rubydex/src/model/definitions.rs +1682 -1602
  60. data/rust/rubydex/src/model/document.rs +222 -252
  61. data/rust/rubydex/src/model/encoding.rs +22 -22
  62. data/rust/rubydex/src/model/graph.rs +3782 -3556
  63. data/rust/rubydex/src/model/id.rs +110 -110
  64. data/rust/rubydex/src/model/identity_maps.rs +58 -58
  65. data/rust/rubydex/src/model/ids.rs +60 -38
  66. data/rust/rubydex/src/model/keywords.rs +256 -256
  67. data/rust/rubydex/src/model/name.rs +298 -298
  68. data/rust/rubydex/src/model/references.rs +111 -111
  69. data/rust/rubydex/src/model/string_ref.rs +50 -50
  70. data/rust/rubydex/src/model/visibility.rs +41 -41
  71. data/rust/rubydex/src/model.rs +15 -14
  72. data/rust/rubydex/src/offset.rs +147 -147
  73. data/rust/rubydex/src/position.rs +6 -6
  74. data/rust/rubydex/src/query.rs +1841 -1700
  75. data/rust/rubydex/src/resolution.rs +1852 -5895
  76. data/rust/rubydex/src/resolution_tests.rs +4701 -0
  77. data/rust/rubydex/src/stats/memory.rs +71 -71
  78. data/rust/rubydex/src/stats/orphan_report.rs +264 -263
  79. data/rust/rubydex/src/stats/timer.rs +127 -127
  80. data/rust/rubydex/src/stats.rs +11 -11
  81. data/rust/rubydex/src/test_utils/context.rs +226 -226
  82. data/rust/rubydex/src/test_utils/graph_test.rs +730 -679
  83. data/rust/rubydex/src/test_utils/local_graph_test.rs +602 -602
  84. data/rust/rubydex/src/test_utils.rs +52 -52
  85. data/rust/rubydex/src/visualization/dot.rs +192 -176
  86. data/rust/rubydex/src/visualization.rs +6 -6
  87. data/rust/rubydex/tests/cli.rs +185 -167
  88. data/rust/rubydex-mcp/Cargo.toml +28 -28
  89. data/rust/rubydex-mcp/src/main.rs +48 -48
  90. data/rust/rubydex-mcp/src/server.rs +1145 -1145
  91. data/rust/rubydex-mcp/src/tools.rs +49 -49
  92. data/rust/rubydex-mcp/tests/mcp.rs +302 -302
  93. data/rust/rubydex-sys/Cargo.toml +20 -20
  94. data/rust/rubydex-sys/build.rs +14 -14
  95. data/rust/rubydex-sys/cbindgen.toml +12 -12
  96. data/rust/rubydex-sys/src/declaration_api.rs +485 -469
  97. data/rust/rubydex-sys/src/definition_api.rs +443 -352
  98. data/rust/rubydex-sys/src/diagnostic_api.rs +99 -99
  99. data/rust/rubydex-sys/src/document_api.rs +85 -54
  100. data/rust/rubydex-sys/src/graph_api.rs +1017 -700
  101. data/rust/rubydex-sys/src/lib.rs +79 -9
  102. data/rust/rubydex-sys/src/location_api.rs +79 -79
  103. data/rust/rubydex-sys/src/name_api.rs +187 -135
  104. data/rust/rubydex-sys/src/reference_api.rs +267 -195
  105. data/rust/rubydex-sys/src/utils.rs +70 -70
  106. data/rust/rustfmt.toml +2 -2
  107. metadata +16 -9
  108. data/lib/rubydex/librubydex_sys.so +0 -0
@@ -1,1700 +1,1841 @@
1
- use std::collections::HashSet;
2
- use std::error::Error;
3
- use std::path::PathBuf;
4
- use std::thread;
5
-
6
- use url::Url;
7
-
8
- use crate::model::declaration::{Ancestor, Declaration};
9
- use crate::model::definitions::{Definition, Parameter};
10
- use crate::model::graph::{Graph, OBJECT_ID};
11
- use crate::model::identity_maps::IdentityHashSet;
12
- use crate::model::ids::{DeclarationId, NameId, StringId, UriId};
13
- use crate::model::keywords::{self, Keyword};
14
- use crate::model::name::NameRef;
15
-
16
- /// Controls how declaration names are matched against the search query.
17
- #[derive(Default)]
18
- pub enum MatchMode {
19
- /// Fuzzy matching: query characters must appear in order in the target (case-insensitive). Used for LSP workspace
20
- /// symbol.
21
- #[default]
22
- Fuzzy,
23
- /// Exact partial matching: query must appear as a contiguous substring of the target. Used for precise filtering
24
- /// (e.g., finding all declarations containing `#is_a?()`).
25
- Exact,
26
- }
27
-
28
- /// # Panics
29
- ///
30
- /// Will panic if any of the threads panic
31
- pub fn declaration_search(graph: &Graph, query: &str, match_mode: &MatchMode) -> Vec<DeclarationId> {
32
- let num_threads = thread::available_parallelism().map(std::num::NonZero::get).unwrap_or(4);
33
- let declarations = graph.declarations();
34
- let ids: Vec<DeclarationId> = declarations.keys().copied().collect();
35
- let chunk_size = ids.len().div_ceil(num_threads);
36
-
37
- if chunk_size == 0 {
38
- return Vec::new();
39
- }
40
-
41
- thread::scope(|s| {
42
- let handles: Vec<_> = ids
43
- .chunks(chunk_size)
44
- .map(|chunk| {
45
- s.spawn(|| {
46
- chunk
47
- .iter()
48
- .filter(|id| {
49
- let declaration = declarations.get(id).unwrap();
50
- let name = declaration.name();
51
- match match_mode {
52
- MatchMode::Fuzzy => {
53
- // When the query is empty, we return everything as per the LSP specification.
54
- // Otherwise, we compute the match score and return anything with a score greater than zero
55
- query.is_empty() || match_score(query, name) > 0
56
- }
57
- MatchMode::Exact => name.contains(query),
58
- }
59
- })
60
- .copied()
61
- .collect::<Vec<_>>()
62
- })
63
- })
64
- .collect();
65
-
66
- handles.into_iter().flat_map(|h| h.join().unwrap()).collect()
67
- })
68
- }
69
-
70
- #[must_use]
71
- fn match_score(query: &str, target: &str) -> usize {
72
- let mut query_chars = query.chars().peekable();
73
- let mut score = 0;
74
-
75
- // Count the number of matches in the order of the query, so that character ordering is taken into account
76
- for t_char in target.chars() {
77
- if let Some(&q_char) = query_chars.peek()
78
- && q_char.eq_ignore_ascii_case(&t_char)
79
- {
80
- score += 1;
81
- query_chars.next();
82
- }
83
- }
84
-
85
- // If after going through the target, there are still query characters left, then some of the query can't be found
86
- // in this target and we return zero to indicate a non-match
87
- if query_chars.peek().is_some() { 0 } else { score }
88
- }
89
-
90
- /// Resolves a require path to its URI ID. Used for go-to-definition.
91
- ///
92
- /// Searches the `load_path` in order and returns the first match, mirroring how Ruby's `require`
93
- /// walks `$LOAD_PATH`.
94
- #[must_use]
95
- pub fn resolve_require_path(graph: &Graph, require_path: &str, load_path: &[PathBuf]) -> Option<UriId> {
96
- let normalized = require_path.trim_end_matches(".rb");
97
-
98
- for path in load_path {
99
- let file_path = path.join(format!("{normalized}.rb"));
100
- let Ok(url) = Url::from_file_path(&file_path) else {
101
- continue;
102
- };
103
- let uri_id = UriId::from(url.as_str());
104
- if graph.documents().contains_key(&uri_id) {
105
- return Some(uri_id);
106
- }
107
- }
108
-
109
- None
110
- }
111
-
112
- /// Returns all require paths. Used for completion.
113
- ///
114
- /// When multiple files resolve to the same require path (e.g., `foo.rb` exists in multiple
115
- /// load paths), the one from the earliest load path wins. This matches Ruby's `require` behavior.
116
- ///
117
- /// # Panics
118
- ///
119
- /// Panics if one of the search threads panics
120
- #[must_use]
121
- pub fn require_paths(graph: &Graph, load_paths: &[PathBuf]) -> Vec<String> {
122
- let num_threads = thread::available_parallelism().map(std::num::NonZero::get).unwrap_or(4);
123
- let documents = graph.documents().iter().collect::<Vec<_>>();
124
- let chunk_size = documents.len().div_ceil(num_threads);
125
-
126
- if chunk_size == 0 {
127
- return Vec::new();
128
- }
129
-
130
- let mut all_results = thread::scope(|scope| {
131
- let handles: Vec<_> = documents
132
- .chunks(chunk_size)
133
- .map(|chunk| {
134
- scope.spawn(move || {
135
- chunk
136
- .iter()
137
- .filter_map(|(_, document)| document.require_path(load_paths))
138
- .collect::<Vec<_>>()
139
- })
140
- })
141
- .collect();
142
-
143
- handles
144
- .into_iter()
145
- .flat_map(|handle| handle.join().unwrap())
146
- .collect::<Vec<_>>()
147
- });
148
-
149
- // Sort by load path index so earlier load paths win during deduplication
150
- all_results.sort_by_key(|(_, index)| *index);
151
-
152
- let mut seen = HashSet::new();
153
- all_results
154
- .into_iter()
155
- .filter(|(require_path, _)| seen.insert(require_path.clone()))
156
- .map(|(require_path, _)| require_path)
157
- .collect()
158
- }
159
-
160
- /// A completion candidate
161
- pub enum CompletionCandidate {
162
- Declaration(DeclarationId),
163
- KeywordArgument(StringId),
164
- Keyword(&'static Keyword),
165
- }
166
-
167
- /// The context in which completion is being requested
168
- pub enum CompletionReceiver {
169
- /// Completion requested for an expression with no previous token (e.g.: at the start of a line with nothing before)
170
- /// Includes: all keywords, all global variables and reacheable instance variables, class variables, constants and methods
171
- Expression(NameId),
172
- /// Completion requested after a namespace access operator (e.g.: `Foo::`)
173
- /// Includes: all constants and singleton methods for the namespace and its ancestors
174
- NamespaceAccess(DeclarationId),
175
- /// Completion requested after a method call operator (e.g.: `foo.`, `@bar.`, `@@baz.`, `Qux.`).
176
- /// In the case of singleton completion (e.g.: `Foo.`), the declaration ID should be for the singleton class (i.e.: `Foo::<Foo>`)
177
- /// Includes: all methods that exist on the type of the receiver and its ancestors
178
- MethodCall(DeclarationId),
179
- /// Completion requested inside a method call's argument list (e.g.: `foo.bar(|)`)
180
- /// Includes: everything expressions do plus keyword parameter names of the method being called
181
- MethodArgument {
182
- self_name_id: NameId,
183
- method_decl_id: DeclarationId,
184
- },
185
- }
186
-
187
- pub struct CompletionContext<'a> {
188
- seen_members: IdentityHashSet<&'a StringId>,
189
- completion_receiver: CompletionReceiver,
190
- }
191
-
192
- impl<'a> CompletionContext<'a> {
193
- #[must_use]
194
- pub fn new(completion_receiver: CompletionReceiver) -> Self {
195
- Self {
196
- seen_members: IdentityHashSet::default(),
197
- completion_receiver,
198
- }
199
- }
200
-
201
- pub fn dedup(&mut self, member_str_id: &'a StringId) -> bool {
202
- self.seen_members.insert(member_str_id)
203
- }
204
- }
205
-
206
- /// Collects completion candidate members
207
- macro_rules! collect_candidates {
208
- // Collect all members with no filtering
209
- ($declaration:expr, $context:expr, $candidates:expr) => {
210
- for (member_str_id, member_declaration_id) in $declaration.members() {
211
- if $context.dedup(member_str_id) {
212
- $candidates.push(CompletionCandidate::Declaration(*member_declaration_id));
213
- }
214
- }
215
- };
216
- // Collect only members matching certain kinds
217
- ($graph:expr, $declaration:expr, $context:expr, $candidates:expr, $kinds:pat) => {
218
- for (member_str_id, member_declaration_id) in $declaration.members() {
219
- let member = $graph.declarations().get(member_declaration_id).unwrap();
220
-
221
- if matches!(member, $kinds) && $context.dedup(member_str_id) {
222
- $candidates.push(CompletionCandidate::Declaration(*member_declaration_id));
223
- }
224
- }
225
- };
226
- }
227
-
228
- /// Determines all possible completion candidates based on the current context of the cursor. There are multiple cases
229
- /// that change what has to be collected for completion:
230
- ///
231
- /// - Expressions collect all keywords, constants, methods, instance variables, class variables, local variables and
232
- /// global variables that are reacheable from the current lexical scope and self type
233
- /// - Expression in method arguments collects everything that expressions do and all keyword parameter names that are
234
- /// applicable to the method being called
235
- /// everything else
236
- /// - Namespace access (e.g.: `Foo::`) collects all constants and singleton methods for the namespace that `Foo`
237
- /// resolves to
238
- /// - Method calls on anything (e.g.: `foo.`, `@bar.`, `@@baz.`, `Qux.`) collects all methods that exist on the type
239
- /// returned by the receiver
240
- ///
241
- /// # Panics
242
- ///
243
- /// Will panic if we incorrectly inserted non namespace declarations as ancestors
244
- ///
245
- /// # Errors
246
- ///
247
- /// Will error if the given `self_name_id` does not point to a namespace declaration
248
- pub fn completion_candidates<'a>(
249
- graph: &'a Graph,
250
- context: CompletionContext<'a>,
251
- ) -> Result<Vec<CompletionCandidate>, Box<dyn Error>> {
252
- match context.completion_receiver {
253
- CompletionReceiver::Expression(self_name_id) => expression_completion(graph, self_name_id, context),
254
- CompletionReceiver::NamespaceAccess(decl_id) => namespace_access_completion(graph, decl_id, context),
255
- CompletionReceiver::MethodCall(decl_id) => method_call_completion(graph, decl_id, context),
256
- CompletionReceiver::MethodArgument {
257
- self_name_id,
258
- method_decl_id,
259
- } => method_argument_completion(graph, self_name_id, method_decl_id, context),
260
- }
261
- }
262
-
263
- /// Resolves a declaration ID to a namespace, following constant aliases if necessary.
264
- fn resolve_to_namespace(graph: &Graph, decl_id: DeclarationId) -> Result<DeclarationId, Box<dyn Error>> {
265
- if let Some(Declaration::Namespace(_)) = graph.declarations().get(&decl_id) {
266
- return Ok(decl_id);
267
- }
268
-
269
- if let Some(target_id) = graph.resolve_alias(&decl_id)
270
- && let Some(Declaration::Namespace(_)) = graph.declarations().get(&target_id)
271
- {
272
- return Ok(target_id);
273
- }
274
-
275
- Err(format!("Expected declaration {decl_id:?} to be a namespace or alias to a namespace").into())
276
- }
277
-
278
- /// Collect completion for a namespace access (e.g.: `Foo::`)
279
- fn namespace_access_completion<'a>(
280
- graph: &'a Graph,
281
- namespace_decl_id: DeclarationId,
282
- mut context: CompletionContext<'a>,
283
- ) -> Result<Vec<CompletionCandidate>, Box<dyn Error>> {
284
- let resolved_id = resolve_to_namespace(graph, namespace_decl_id)?;
285
- let namespace = graph.declarations().get(&resolved_id).unwrap().as_namespace().unwrap();
286
- let mut candidates = Vec::new();
287
-
288
- // Walk ancestors collecting inherited constants, stopping at Object to avoid surfacing top-level constants
289
- // from Object, Kernel, BasicObject, etc.
290
- for ancestor in namespace.ancestors() {
291
- if let Ancestor::Complete(ancestor_id) = ancestor {
292
- // Do not offer completion for constants inherited after `Object` (e.g.: `Object::String`). While this is
293
- // valid Ruby code, it's extremely uncommon and not a super valuable completion suggestion
294
- if *ancestor_id == *OBJECT_ID {
295
- break;
296
- }
297
-
298
- let ancestor_decl = graph.declarations().get(ancestor_id).unwrap().as_namespace().unwrap();
299
-
300
- collect_candidates!(
301
- graph,
302
- &ancestor_decl,
303
- context,
304
- candidates,
305
- Declaration::Namespace(_) | Declaration::Constant(_) | Declaration::ConstantAlias(_)
306
- );
307
- }
308
- }
309
-
310
- // Collect singleton methods from the singleton class and its ancestors
311
- if let Some(singleton_id) = namespace.singleton_class() {
312
- let singleton = graph.declarations().get(singleton_id).unwrap().as_namespace().unwrap();
313
-
314
- for ancestor in singleton.ancestors() {
315
- if let Ancestor::Complete(ancestor_id) = ancestor {
316
- let ancestor_decl = graph.declarations().get(ancestor_id).unwrap().as_namespace().unwrap();
317
- collect_candidates!(graph, &ancestor_decl, context, candidates, Declaration::Method(_));
318
- }
319
- }
320
- }
321
-
322
- Ok(candidates)
323
- }
324
-
325
- /// Collect completion for a method call (e.g.: `foo.`, `@bar.`, `Baz.`)
326
- fn method_call_completion<'a>(
327
- graph: &'a Graph,
328
- receiver_decl_id: DeclarationId,
329
- mut context: CompletionContext<'a>,
330
- ) -> Result<Vec<CompletionCandidate>, Box<dyn Error>> {
331
- let resolved_id = resolve_to_namespace(graph, receiver_decl_id)?;
332
- let namespace = graph.declarations().get(&resolved_id).unwrap().as_namespace().unwrap();
333
- let mut candidates = Vec::new();
334
-
335
- for ancestor in namespace.ancestors() {
336
- if let Ancestor::Complete(ancestor_id) = ancestor {
337
- let ancestor_decl = graph.declarations().get(ancestor_id).unwrap().as_namespace().unwrap();
338
- collect_candidates!(graph, &ancestor_decl, context, candidates, Declaration::Method(_));
339
- }
340
- }
341
-
342
- Ok(candidates)
343
- }
344
-
345
- /// Collect completion for an expression
346
- fn expression_completion<'a>(
347
- graph: &'a Graph,
348
- self_name_id: NameId,
349
- mut context: CompletionContext<'a>,
350
- ) -> Result<Vec<CompletionCandidate>, Box<dyn Error>> {
351
- let Some(name_ref) = graph.names().get(&self_name_id) else {
352
- return Err(format!("Name {self_name_id} not found in graph").into());
353
- };
354
- let NameRef::Resolved(name_ref) = name_ref else {
355
- return Err(format!("Expected name {self_name_id} to be resolved").into());
356
- };
357
- let Some(self_decl) = graph
358
- .declarations()
359
- .get(name_ref.declaration_id())
360
- .and_then(|d| d.as_namespace())
361
- else {
362
- return Err("Expected associated declaration to be a namespace".into());
363
- };
364
- let mut candidates = Vec::new();
365
-
366
- // Walk the name's lexical scopes, collecting all constant completion members
367
- let mut current_name_id = Some(self_name_id);
368
-
369
- while let Some(id) = current_name_id {
370
- let NameRef::Resolved(name_ref) = graph.names().get(&id).unwrap() else {
371
- break;
372
- };
373
-
374
- let nesting_decl = graph
375
- .declarations()
376
- .get(name_ref.declaration_id())
377
- .unwrap()
378
- .as_namespace()
379
- .unwrap();
380
-
381
- collect_candidates!(
382
- graph,
383
- &nesting_decl,
384
- context,
385
- candidates,
386
- Declaration::Namespace(_) | Declaration::Constant(_) | Declaration::ConstantAlias(_)
387
- );
388
-
389
- current_name_id = *name_ref.nesting();
390
- }
391
-
392
- // Include all top level constants and globals, which are accessible everywhere
393
- let object = graph.declarations().get(&OBJECT_ID).unwrap().as_namespace().unwrap();
394
- collect_candidates!(
395
- graph,
396
- &object,
397
- context,
398
- candidates,
399
- Declaration::Namespace(_)
400
- | Declaration::Constant(_)
401
- | Declaration::ConstantAlias(_)
402
- | Declaration::GlobalVariable(_)
403
- );
404
-
405
- // Walk ancestors collecting all applicable completion members
406
- for ancestor in self_decl.ancestors() {
407
- if let Ancestor::Complete(ancestor_id) = ancestor {
408
- let ancestor_decl = graph.declarations().get(ancestor_id).unwrap().as_namespace().unwrap();
409
- collect_candidates!(&ancestor_decl, context, candidates);
410
-
411
- // Collect class variables from the attached object, which are available at any singleton class level
412
- // within self
413
- let attached_object = graph.attached_object(ancestor_decl);
414
- collect_candidates!(
415
- graph,
416
- &attached_object,
417
- context,
418
- candidates,
419
- Declaration::ClassVariable(_)
420
- );
421
- }
422
- }
423
-
424
- // Keywords are always available in expression contexts
425
- candidates.extend(keywords::KEYWORDS.iter().map(CompletionCandidate::Keyword));
426
- Ok(candidates)
427
- }
428
-
429
- /// Collect completion for a method argument (e.g.: `foo.bar(|)`)
430
- fn method_argument_completion<'a>(
431
- graph: &'a Graph,
432
- self_name_id: NameId,
433
- method_decl_id: DeclarationId,
434
- context: CompletionContext<'a>,
435
- ) -> Result<Vec<CompletionCandidate>, Box<dyn Error>> {
436
- let mut candidates = expression_completion(graph, self_name_id, context)?;
437
- let Some(method_decl) = graph.declarations().get(&method_decl_id) else {
438
- return Ok(candidates);
439
- };
440
-
441
- // Find the first Method definition to extract keyword parameters
442
- for def_id in method_decl.definitions() {
443
- if let Some(Definition::Method(method_def)) = graph.definitions().get(def_id) {
444
- for signature in method_def.signatures().as_slice() {
445
- for param in signature {
446
- match param {
447
- Parameter::RequiredKeyword(p) | Parameter::OptionalKeyword(p) => {
448
- candidates.push(CompletionCandidate::KeywordArgument(*p.str()));
449
- }
450
- _ => {}
451
- }
452
- }
453
- }
454
- break;
455
- }
456
- }
457
-
458
- Ok(candidates)
459
- }
460
-
461
- #[cfg(test)]
462
- mod tests {
463
- use std::str::FromStr;
464
- use url::Url;
465
-
466
- use super::*;
467
- use crate::{
468
- model::{
469
- ids::StringId,
470
- name::{Name, ParentScope},
471
- },
472
- test_utils::GraphTest,
473
- };
474
-
475
- macro_rules! assert_results_eq {
476
- ($context:expr, $query:expr, $expected:expr) => {
477
- assert_results_eq!($context, $query, &MatchMode::default(), $expected);
478
- };
479
- ($context:expr, $query:expr, $match_mode:expr, $expected:expr) => {
480
- let actual = declaration_search(&$context.graph(), $query, $match_mode);
481
- assert_eq!(
482
- actual,
483
- $expected
484
- .into_iter()
485
- .map(|s| DeclarationId::from(s))
486
- .collect::<Vec<DeclarationId>>(),
487
- "Unexpected search results: {:?}",
488
- actual
489
- .iter()
490
- .map(|id| $context
491
- .graph()
492
- .declarations()
493
- .get(id)
494
- .unwrap()
495
- .name()
496
- .to_string())
497
- .collect::<Vec<String>>()
498
- );
499
- };
500
- }
501
-
502
- fn candidate_label(context: &GraphTest, candidate: &CompletionCandidate) -> String {
503
- match candidate {
504
- CompletionCandidate::Declaration(id) => context.graph().declarations().get(id).unwrap().name().to_string(),
505
- CompletionCandidate::KeywordArgument(str_id) => {
506
- format!("{}:", context.graph().strings().get(str_id).unwrap().as_str())
507
- }
508
- CompletionCandidate::Keyword(kw) => kw.name().to_string(),
509
- }
510
- }
511
-
512
- macro_rules! assert_completion_eq {
513
- ($context:expr, $receiver:expr, $expected:expr) => {
514
- assert_eq!(
515
- $expected,
516
- *completion_candidates($context.graph(), CompletionContext::new($receiver))
517
- .unwrap()
518
- .iter()
519
- .map(|candidate| candidate_label(&$context, candidate))
520
- .collect::<Vec<_>>()
521
- );
522
- };
523
- }
524
-
525
- /// Asserts declaration and keyword argument completion candidates, excluding language keywords.
526
- /// Language keywords are always present in expression contexts and tested separately.
527
- macro_rules! assert_declaration_completion_eq {
528
- ($context:expr, $receiver:expr, $expected:expr) => {
529
- assert_eq!(
530
- $expected,
531
- *completion_candidates($context.graph(), CompletionContext::new($receiver))
532
- .unwrap()
533
- .iter()
534
- .filter(|c| !matches!(c, CompletionCandidate::Keyword(_)))
535
- .map(|candidate| candidate_label(&$context, candidate))
536
- .collect::<Vec<_>>()
537
- );
538
- };
539
- }
540
-
541
- #[test]
542
- fn fuzzy_search_returns_partial_matches() {
543
- let mut context = GraphTest::new();
544
- context.index_uri("file:///foo.rb", {
545
- r"
546
- class Foo
547
- end
548
- "
549
- });
550
- context.resolve();
551
- assert_results_eq!(context, "Fo", ["Foo"]);
552
- }
553
-
554
- #[test]
555
- fn exact_partial_match_search() {
556
- let mut context = GraphTest::new();
557
- context.index_uri("file:///foo.rb", {
558
- r"
559
- class Foo
560
- def is_a_foo?; end
561
- end
562
-
563
- class Bar < Foo
564
- def is_a?(other); end
565
- end
566
- "
567
- });
568
- context.resolve();
569
- assert_results_eq!(context, "#is_a?()", &MatchMode::Exact, ["Bar#is_a?()"]);
570
- }
571
-
572
- #[test]
573
- fn exact_match_empty_query_returns_all() {
574
- let mut context = GraphTest::new();
575
- context.index_uri("file:///foo.rb", {
576
- r"
577
- class Foo; end
578
- class Bar; end
579
- "
580
- });
581
- context.resolve();
582
- let exact_results = declaration_search(context.graph(), "", &MatchMode::Exact);
583
- let fuzzy_results = declaration_search(context.graph(), "", &MatchMode::Fuzzy);
584
-
585
- assert_eq!(exact_results.len(), fuzzy_results.len());
586
- assert_eq!(context.graph().declarations().len(), exact_results.len());
587
- }
588
-
589
- #[test]
590
- fn exact_match_is_case_sensitive() {
591
- let mut context = GraphTest::new();
592
- context.index_uri("file:///foo.rb", {
593
- r"
594
- class Foo
595
- def is_a_foo?; end
596
- end
597
-
598
- class Bar < Foo
599
- def is_a?(other); end
600
- end
601
- "
602
- });
603
- context.resolve();
604
-
605
- assert_results_eq!(context, "#Is_A?()", &MatchMode::Exact, Vec::<&str>::new());
606
- assert_results_eq!(context, "#Is_A?()", ["Foo#is_a_foo?()", "Bar#is_a?()"]);
607
- }
608
-
609
- fn test_root() -> PathBuf {
610
- let root = if cfg!(windows) { "C:\\" } else { "/" };
611
- PathBuf::from_str(root).unwrap()
612
- }
613
-
614
- #[test]
615
- fn test_resolve_require_path() {
616
- let root = test_root();
617
- let path = root
618
- .join("lib")
619
- .join("foo")
620
- .join("bar.rb")
621
- .to_str()
622
- .unwrap()
623
- .to_string();
624
- let uri = Url::from_file_path(path).unwrap().to_string();
625
- let load_paths = [root.join("lib")];
626
-
627
- let mut context = GraphTest::new();
628
- context.index_uri(&uri, "class Bar; end");
629
-
630
- // finds basic path
631
- let uri_id = resolve_require_path(context.graph(), "foo/bar", &load_paths);
632
- assert!(uri_id.is_some());
633
- let doc = context.graph().documents().get(&uri_id.unwrap()).unwrap();
634
- assert_eq!(uri, doc.uri());
635
-
636
- // handles .rb suffix
637
- let uri_id_with_rb = resolve_require_path(context.graph(), "foo/bar.rb", &load_paths);
638
- assert_eq!(uri_id, uri_id_with_rb);
639
-
640
- // returns None for nonexistent
641
- assert!(resolve_require_path(context.graph(), "nonexistent", &load_paths).is_none());
642
- }
643
-
644
- #[test]
645
- fn test_resolve_require_path_prefers_earliest_load_path() {
646
- let root = test_root();
647
- let lib_path = root.join("lib").join("foo").join("bar.rb");
648
- let test_path = root.join("test").join("foo").join("bar.rb");
649
- let lib_uri = Url::from_file_path(&lib_path).unwrap().to_string();
650
- let test_uri = Url::from_file_path(&test_path).unwrap().to_string();
651
-
652
- let mut context = GraphTest::new();
653
- context.index_uri(&lib_uri, "class Bar; end");
654
- context.index_uri(&test_uri, "class Bar; end");
655
-
656
- // lib comes first in load paths
657
- let load_paths = [root.join("lib"), root.join("test")];
658
- let uri_id = resolve_require_path(context.graph(), "foo/bar", &load_paths).unwrap();
659
- let doc = context.graph().documents().get(&uri_id).unwrap();
660
- assert!(
661
- doc.uri().contains("lib/foo/bar.rb"),
662
- "Expected lib path, got {}",
663
- doc.uri()
664
- );
665
-
666
- // test comes first in load paths
667
- let load_paths = [root.join("test"), root.join("lib")];
668
- let uri_id = resolve_require_path(context.graph(), "foo/bar", &load_paths).unwrap();
669
- let doc = context.graph().documents().get(&uri_id).unwrap();
670
- assert!(
671
- doc.uri().contains("test/foo/bar.rb"),
672
- "Expected test path, got {}",
673
- doc.uri()
674
- );
675
- }
676
-
677
- #[test]
678
- fn test_require_paths() {
679
- let root = test_root();
680
- let path_bar = root.join("lib").join("foo").join("bar.rb");
681
- let path_qux = root.join("lib").join("foo").join("qux.rb");
682
- let path_foobar = root.join("lib").join("foobar.rb");
683
- let uri_bar = Url::from_file_path(&path_bar).unwrap().to_string();
684
- let uri_qux = Url::from_file_path(&path_qux).unwrap().to_string();
685
- let uri_foobar = Url::from_file_path(&path_foobar).unwrap().to_string();
686
- let load_paths = vec![root.join("lib")];
687
-
688
- let mut context = GraphTest::new();
689
- context.index_uri(&uri_bar, "class Bar; end");
690
- context.index_uri(&uri_qux, "class Qux; end");
691
- context.index_uri(&uri_foobar, "class Foobar; end");
692
-
693
- let results = require_paths(context.graph(), &load_paths);
694
-
695
- assert_eq!(3, results.len());
696
- assert!(results.contains(&"foo/bar".to_string()));
697
- assert!(results.contains(&"foo/qux".to_string()));
698
- assert!(results.contains(&"foobar".to_string()));
699
- }
700
-
701
- #[test]
702
- fn test_require_paths_deduplicates_by_load_path_order() {
703
- let root = test_root();
704
- let path1 = root.join("lib1").join("foo.rb");
705
- let path2 = root.join("lib2").join("foo.rb");
706
- let uri1 = Url::from_file_path(&path1).unwrap().to_string();
707
- let uri2 = Url::from_file_path(&path2).unwrap().to_string();
708
- let load_paths = [root.join("lib1"), root.join("lib2")];
709
-
710
- let mut context = GraphTest::new();
711
- context.index_uri(&uri1, "class Foo; end");
712
- context.index_uri(&uri2, "class Foo; end");
713
-
714
- let results = require_paths(context.graph(), &load_paths);
715
-
716
- let foo_count = results.iter().filter(|p| *p == "foo").count();
717
- assert_eq!(1, foo_count);
718
- }
719
-
720
- #[test]
721
- fn completion_candidates_on_self() {
722
- let mut context = GraphTest::new();
723
-
724
- context.index_uri(
725
- "file:///foo.rb",
726
- "
727
- module Foo
728
- CONST = 1
729
- def bar; end
730
- end
731
-
732
- class Parent
733
- def initialize
734
- @var = 1
735
- end
736
- end
737
-
738
- class Child < Parent
739
- include Foo
740
-
741
- def baz
742
- # Completion in this `self` context
743
- end
744
- end
745
- ",
746
- );
747
- context.resolve();
748
-
749
- let name_id = Name::new(StringId::from("Child"), ParentScope::None, None).id();
750
- assert_declaration_completion_eq!(
751
- context,
752
- CompletionReceiver::Expression(name_id),
753
- [
754
- "Child",
755
- "Foo",
756
- "Parent",
757
- "Child#baz()",
758
- "Foo::CONST",
759
- "Foo#bar()",
760
- "Parent#initialize()",
761
- "Parent#@var"
762
- ]
763
- );
764
- }
765
-
766
- #[test]
767
- fn completion_candidates_shows_first_option_in_the_ancestor_chain() {
768
- let mut context = GraphTest::new();
769
-
770
- context.index_uri(
771
- "file:///foo.rb",
772
- "
773
- module Foo
774
- def bar; end
775
- end
776
-
777
- class Parent
778
- def bar; end
779
- end
780
-
781
- class Child < Parent
782
- def bar
783
- # Completion in this `self` context
784
- end
785
- end
786
- ",
787
- );
788
- context.resolve();
789
-
790
- let name_id = Name::new(StringId::from("Child"), ParentScope::None, None).id();
791
- assert_declaration_completion_eq!(
792
- context,
793
- CompletionReceiver::Expression(name_id),
794
- ["Child", "Foo", "Parent", "Child#bar()"]
795
- );
796
- }
797
-
798
- #[test]
799
- fn completion_candidates_in_a_cyclic_ancestor_chain() {
800
- let mut context = GraphTest::new();
801
-
802
- context.index_uri(
803
- "file:///foo.rb",
804
- "
805
- module Foo
806
- include Baz
807
-
808
- def foo_m; end
809
- end
810
-
811
- module Bar
812
- include Foo
813
-
814
- def bar_m; end
815
- end
816
-
817
- module Baz
818
- include Bar
819
-
820
- def baz_m; end
821
- end
822
- ",
823
- );
824
- context.resolve();
825
-
826
- let name_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
827
- assert_declaration_completion_eq!(
828
- context,
829
- CompletionReceiver::Expression(name_id),
830
- ["Baz", "Foo", "Bar", "Foo#foo_m()", "Baz#baz_m()", "Bar#bar_m()"]
831
- );
832
- }
833
-
834
- #[test]
835
- fn completion_candidates_for_class_variables() {
836
- let mut context = GraphTest::new();
837
-
838
- context.index_uri(
839
- "file:///foo.rb",
840
- "
841
- class Foo
842
- @@foo_var = 1
843
-
844
- class << self
845
- def do_something
846
- # Completion in this `self` context
847
- end
848
- end
849
- end
850
-
851
- class Bar < Foo
852
- def baz
853
- # Other completion in this `self` context
854
- end
855
- end
856
- ",
857
- );
858
- context.resolve();
859
-
860
- let foo_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
861
- let name_id = Name::new(StringId::from("<Foo>"), ParentScope::Attached(foo_id), None).id();
862
- assert_declaration_completion_eq!(
863
- context,
864
- CompletionReceiver::Expression(name_id),
865
- ["Foo", "Bar", "Foo::<Foo>#do_something()", "Foo#@@foo_var"]
866
- );
867
-
868
- let name_id = Name::new(StringId::from("Bar"), ParentScope::None, None).id();
869
- assert_declaration_completion_eq!(
870
- context,
871
- CompletionReceiver::Expression(name_id),
872
- ["Foo", "Bar", "Bar#baz()", "Foo#@@foo_var"]
873
- );
874
- }
875
-
876
- #[test]
877
- fn completion_candidates_includes_constants_accessible_within_lexical_scope() {
878
- let mut context = GraphTest::new();
879
-
880
- context.index_uri(
881
- "file:///foo.rb",
882
- "
883
- module Foo
884
- CONST_A = 1
885
-
886
- class ::Bar
887
- def bar_m
888
- # Completion in this `self` context
889
- end
890
- end
891
- end
892
-
893
- class Bar
894
- def bar_m2
895
- # Completion in this `self` context
896
- end
897
- end
898
- ",
899
- );
900
- context.resolve();
901
-
902
- let name_id = Name::new(
903
- StringId::from("Bar"),
904
- ParentScope::TopLevel,
905
- Some(Name::new(StringId::from("Foo"), ParentScope::None, None).id()),
906
- )
907
- .id();
908
- assert_declaration_completion_eq!(
909
- context,
910
- CompletionReceiver::Expression(name_id),
911
- ["Foo::CONST_A", "Foo", "Bar", "Bar#bar_m()", "Bar#bar_m2()"]
912
- );
913
-
914
- let name_id = Name::new(StringId::from("Bar"), ParentScope::None, None).id();
915
- assert_declaration_completion_eq!(
916
- context,
917
- CompletionReceiver::Expression(name_id),
918
- ["Foo", "Bar", "Bar#bar_m()", "Bar#bar_m2()"]
919
- );
920
- }
921
-
922
- #[test]
923
- fn completion_candidates_finds_unqualified_constant_reachable_from_namespace() {
924
- let mut context = GraphTest::new();
925
-
926
- context.index_uri(
927
- "file:///foo.rb",
928
- "
929
- module Foo
930
- CONST = 1
931
-
932
- class Bar
933
- def baz
934
- # Typing CONST here should find Foo::CONST
935
- end
936
- end
937
- end
938
- ",
939
- );
940
- context.resolve();
941
-
942
- let foo_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
943
- let name_id = Name::new(StringId::from("Bar"), ParentScope::None, Some(foo_id)).id();
944
- // Foo::CONST is reachable from Foo::Bar through lexical scoping, so it must appear as a completion candidate
945
- // when the user types the unqualified name CONST
946
- assert_declaration_completion_eq!(
947
- context,
948
- CompletionReceiver::Expression(name_id),
949
- ["Foo::CONST", "Foo::Bar", "Foo", "Foo::Bar#baz()"]
950
- );
951
- }
952
-
953
- #[test]
954
- fn completion_candidates_includes_globals() {
955
- let mut context = GraphTest::new();
956
-
957
- context.index_uri(
958
- "file:///foo.rb",
959
- "
960
- $var = 1
961
- module Foo
962
- $var2 = 2
963
-
964
- class Bar < BasicObject
965
- def bar_m
966
- # Completion in this `self` context
967
- end
968
- end
969
- end
970
- ",
971
- );
972
- context.resolve();
973
-
974
- let name_id = Name::new(
975
- StringId::from("Bar"),
976
- ParentScope::None,
977
- Some(Name::new(StringId::from("Foo"), ParentScope::None, None).id()),
978
- )
979
- .id();
980
- assert_declaration_completion_eq!(
981
- context,
982
- CompletionReceiver::Expression(name_id),
983
- ["Foo::Bar", "$var", "Foo", "$var2", "Foo::Bar#bar_m()"]
984
- );
985
- }
986
-
987
- #[test]
988
- fn namespace_access_completion_collects_constants_and_singleton_methods() {
989
- let mut context = GraphTest::new();
990
-
991
- context.index_uri(
992
- "file:///foo.rb",
993
- "
994
- module Foo
995
- CONST = 1
996
- class Bar; end
997
-
998
- class << self
999
- def class_method; end
1000
- end
1001
-
1002
- def instance_method; end
1003
- end
1004
- ",
1005
- );
1006
- context.resolve();
1007
-
1008
- assert_completion_eq!(
1009
- context,
1010
- CompletionReceiver::NamespaceAccess(DeclarationId::from("Foo")),
1011
- ["Foo::CONST", "Foo::Bar", "Foo::<Foo>#class_method()"]
1012
- );
1013
- }
1014
-
1015
- #[test]
1016
- fn namespace_access_completion_includes_inherited_members() {
1017
- let mut context = GraphTest::new();
1018
-
1019
- context.index_uri(
1020
- "file:///foo.rb",
1021
- "
1022
- class Parent
1023
- PARENT_CONST = 1
1024
-
1025
- class << self
1026
- def parent_class_method; end
1027
- end
1028
- end
1029
-
1030
- class Child < Parent
1031
- CHILD_CONST = 2
1032
-
1033
- class << self
1034
- def child_class_method; end
1035
- end
1036
- end
1037
- ",
1038
- );
1039
- context.resolve();
1040
-
1041
- assert_completion_eq!(
1042
- context,
1043
- CompletionReceiver::NamespaceAccess(DeclarationId::from("Child")),
1044
- [
1045
- "Child::CHILD_CONST",
1046
- "Parent::PARENT_CONST",
1047
- "Child::<Child>#child_class_method()",
1048
- "Parent::<Parent>#parent_class_method()",
1049
- ]
1050
- );
1051
- }
1052
-
1053
- #[test]
1054
- fn namespace_access_completion_deduplicates_overridden_members() {
1055
- let mut context = GraphTest::new();
1056
-
1057
- context.index_uri(
1058
- "file:///foo.rb",
1059
- "
1060
- class Parent
1061
- CONST = 1
1062
-
1063
- class << self
1064
- def shared_method; end
1065
- end
1066
- end
1067
-
1068
- class Child < Parent
1069
- CONST = 2
1070
-
1071
- class << self
1072
- def shared_method; end
1073
- end
1074
- end
1075
- ",
1076
- );
1077
- context.resolve();
1078
-
1079
- assert_completion_eq!(
1080
- context,
1081
- CompletionReceiver::NamespaceAccess(DeclarationId::from("Child")),
1082
- ["Child::CONST", "Child::<Child>#shared_method()"]
1083
- );
1084
- }
1085
-
1086
- #[test]
1087
- fn namespace_access_completion_excludes_object_owned_constants() {
1088
- let mut context = GraphTest::new();
1089
-
1090
- context.index_uri(
1091
- "file:///foo.rb",
1092
- "
1093
- class Foo
1094
- CONST = 1
1095
- end
1096
-
1097
- class Bar; end
1098
- ",
1099
- );
1100
- context.resolve();
1101
-
1102
- assert_completion_eq!(
1103
- context,
1104
- CompletionReceiver::NamespaceAccess(DeclarationId::from("Foo")),
1105
- ["Foo::CONST"]
1106
- );
1107
- }
1108
-
1109
- #[test]
1110
- fn namespace_access_completion_includes_constant_aliases() {
1111
- let mut context = GraphTest::new();
1112
-
1113
- context.index_uri(
1114
- "file:///foo.rb",
1115
- "
1116
- module Foo
1117
- Bar = String
1118
- CONST = 1
1119
- end
1120
- ",
1121
- );
1122
- context.resolve();
1123
-
1124
- assert_completion_eq!(
1125
- context,
1126
- CompletionReceiver::NamespaceAccess(DeclarationId::from("Foo")),
1127
- ["Foo::CONST", "Foo::Bar"]
1128
- );
1129
- }
1130
-
1131
- #[test]
1132
- fn namespace_access_completion_follows_constant_alias() {
1133
- let mut context = GraphTest::new();
1134
-
1135
- context.index_uri(
1136
- "file:///foo.rb",
1137
- "
1138
- class Original
1139
- CONST = 1
1140
- class Nested; end
1141
-
1142
- class << self
1143
- def class_method; end
1144
- end
1145
- end
1146
-
1147
- module Foo
1148
- MyOriginal = Original
1149
- end
1150
- ",
1151
- );
1152
- context.resolve();
1153
-
1154
- assert_completion_eq!(
1155
- context,
1156
- CompletionReceiver::NamespaceAccess(DeclarationId::from("Foo::MyOriginal")),
1157
- [
1158
- "Original::CONST",
1159
- "Original::Nested",
1160
- "Original::<Original>#class_method()"
1161
- ]
1162
- );
1163
- }
1164
-
1165
- #[test]
1166
- fn namespace_access_completion_follows_chained_constant_alias() {
1167
- let mut context = GraphTest::new();
1168
-
1169
- context.index_uri(
1170
- "file:///foo.rb",
1171
- "
1172
- class Original
1173
- CONST = 1
1174
-
1175
- class << self
1176
- def class_method; end
1177
- end
1178
- end
1179
-
1180
- Alias1 = Original
1181
- Alias2 = Alias1
1182
- ",
1183
- );
1184
- context.resolve();
1185
-
1186
- assert_completion_eq!(
1187
- context,
1188
- CompletionReceiver::NamespaceAccess(DeclarationId::from("Alias2")),
1189
- ["Original::CONST", "Original::<Original>#class_method()"]
1190
- );
1191
- }
1192
-
1193
- #[test]
1194
- fn namespace_access_completion_on_basic_object_subclass() {
1195
- let mut context = GraphTest::new();
1196
-
1197
- context.index_uri(
1198
- "file:///foo.rb",
1199
- "
1200
- class Foo < BasicObject
1201
- CONST = 1
1202
-
1203
- class << self
1204
- def class_method; end
1205
- end
1206
- end
1207
-
1208
- class Bar; end
1209
- ",
1210
- );
1211
- context.resolve();
1212
-
1213
- assert_completion_eq!(
1214
- context,
1215
- CompletionReceiver::NamespaceAccess(DeclarationId::from("Foo")),
1216
- ["Foo::CONST", "Foo::<Foo>#class_method()"]
1217
- );
1218
- }
1219
-
1220
- #[test]
1221
- fn namespace_access_completion_includes_module_members() {
1222
- let mut context = GraphTest::new();
1223
-
1224
- context.index_uri(
1225
- "file:///foo.rb",
1226
- "
1227
- module Bar
1228
- CONST = 1
1229
-
1230
- class << self
1231
- def bar_class_method; end
1232
- end
1233
- end
1234
-
1235
- class Foo
1236
- FOO_CONST = 2
1237
- include Bar
1238
-
1239
- class << self
1240
- def foo_class_method; end
1241
- end
1242
- end
1243
- ",
1244
- );
1245
- context.resolve();
1246
-
1247
- assert_completion_eq!(
1248
- context,
1249
- CompletionReceiver::NamespaceAccess(DeclarationId::from("Foo")),
1250
- ["Foo::FOO_CONST", "Bar::CONST", "Foo::<Foo>#foo_class_method()"]
1251
- );
1252
- }
1253
-
1254
- #[test]
1255
- fn method_call_completion_collects_instance_methods() {
1256
- let mut context = GraphTest::new();
1257
-
1258
- context.index_uri(
1259
- "file:///foo.rb",
1260
- "
1261
- class Foo
1262
- CONST = 1
1263
-
1264
- def bar; end
1265
- def baz; end
1266
-
1267
- class << self
1268
- def class_method; end
1269
- end
1270
- end
1271
- ",
1272
- );
1273
- context.resolve();
1274
-
1275
- assert_completion_eq!(
1276
- context,
1277
- CompletionReceiver::MethodCall(DeclarationId::from("Foo")),
1278
- ["Foo#baz()", "Foo#bar()"]
1279
- );
1280
- }
1281
-
1282
- #[test]
1283
- fn method_call_completion_follows_constant_alias() {
1284
- let mut context = GraphTest::new();
1285
-
1286
- context.index_uri(
1287
- "file:///foo.rb",
1288
- "
1289
- class Original
1290
- def bar; end
1291
- def baz; end
1292
-
1293
- class << self
1294
- def class_method; end
1295
- end
1296
- end
1297
-
1298
- module Foo
1299
- MyOriginal = Original
1300
- end
1301
- ",
1302
- );
1303
- context.resolve();
1304
-
1305
- assert_completion_eq!(
1306
- context,
1307
- CompletionReceiver::MethodCall(DeclarationId::from("Foo::MyOriginal")),
1308
- ["Original#baz()", "Original#bar()"]
1309
- );
1310
- }
1311
-
1312
- #[test]
1313
- fn method_call_completion_includes_inherited_methods() {
1314
- let mut context = GraphTest::new();
1315
-
1316
- context.index_uri(
1317
- "file:///foo.rb",
1318
- "
1319
- class Parent
1320
- def parent_method; end
1321
- end
1322
-
1323
- class Child < Parent
1324
- def child_method; end
1325
- end
1326
- ",
1327
- );
1328
- context.resolve();
1329
-
1330
- assert_completion_eq!(
1331
- context,
1332
- CompletionReceiver::MethodCall(DeclarationId::from("Child")),
1333
- ["Child#child_method()", "Parent#parent_method()"]
1334
- );
1335
- }
1336
-
1337
- #[test]
1338
- fn method_call_completion_includes_methods_from_included_modules() {
1339
- let mut context = GraphTest::new();
1340
-
1341
- context.index_uri(
1342
- "file:///foo.rb",
1343
- "
1344
- module Mixin
1345
- def mixin_method; end
1346
- end
1347
-
1348
- class Foo
1349
- include Mixin
1350
-
1351
- def foo_method; end
1352
- end
1353
- ",
1354
- );
1355
- context.resolve();
1356
-
1357
- assert_completion_eq!(
1358
- context,
1359
- CompletionReceiver::MethodCall(DeclarationId::from("Foo")),
1360
- ["Foo#foo_method()", "Mixin#mixin_method()"]
1361
- );
1362
- }
1363
-
1364
- #[test]
1365
- fn method_call_completion_deduplicates_overridden_methods() {
1366
- let mut context = GraphTest::new();
1367
-
1368
- context.index_uri(
1369
- "file:///foo.rb",
1370
- "
1371
- class Parent
1372
- def shared_method; end
1373
- def parent_only; end
1374
- end
1375
-
1376
- class Child < Parent
1377
- def shared_method; end
1378
- def child_only; end
1379
- end
1380
- ",
1381
- );
1382
- context.resolve();
1383
-
1384
- assert_completion_eq!(
1385
- context,
1386
- CompletionReceiver::MethodCall(DeclarationId::from("Child")),
1387
- ["Child#shared_method()", "Child#child_only()", "Parent#parent_only()"]
1388
- );
1389
- }
1390
-
1391
- #[test]
1392
- fn method_call_completion_excludes_non_method_members() {
1393
- let mut context = GraphTest::new();
1394
-
1395
- context.index_uri(
1396
- "file:///foo.rb",
1397
- "
1398
- class Foo
1399
- CONST = 1
1400
- @@class_var = 2
1401
-
1402
- def initialize
1403
- @ivar = 3
1404
- end
1405
-
1406
- def bar; end
1407
- end
1408
- ",
1409
- );
1410
- context.resolve();
1411
-
1412
- assert_completion_eq!(
1413
- context,
1414
- CompletionReceiver::MethodCall(DeclarationId::from("Foo")),
1415
- ["Foo#initialize()", "Foo#bar()"]
1416
- );
1417
- }
1418
-
1419
- #[test]
1420
- fn method_call_completion_at_singleton_level() {
1421
- let mut context = GraphTest::new();
1422
-
1423
- context.index_uri(
1424
- "file:///foo.rb",
1425
- "
1426
- class Foo
1427
- def self.bar; end
1428
-
1429
- class << self
1430
- def baz; end
1431
- end
1432
- end
1433
- ",
1434
- );
1435
- context.resolve();
1436
-
1437
- assert_completion_eq!(
1438
- context,
1439
- CompletionReceiver::MethodCall(DeclarationId::from("Foo::<Foo>")),
1440
- ["Foo::<Foo>#baz()", "Foo::<Foo>#bar()"]
1441
- );
1442
- }
1443
-
1444
- #[test]
1445
- fn method_argument_completion_includes_keyword_params() {
1446
- let mut context = GraphTest::new();
1447
-
1448
- context.index_uri(
1449
- "file:///foo.rb",
1450
- "
1451
- class Foo
1452
- def greet(name:, greeting: 'hello'); end
1453
- end
1454
- ",
1455
- );
1456
- context.resolve();
1457
-
1458
- let name_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
1459
- assert_declaration_completion_eq!(
1460
- context,
1461
- CompletionReceiver::MethodArgument {
1462
- self_name_id: name_id,
1463
- method_decl_id: DeclarationId::from("Foo#greet()"),
1464
- },
1465
- ["Foo", "Foo#greet()", "name:", "greeting:"]
1466
- );
1467
- }
1468
-
1469
- #[test]
1470
- fn method_argument_completion_no_keyword_params() {
1471
- let mut context = GraphTest::new();
1472
-
1473
- context.index_uri(
1474
- "file:///foo.rb",
1475
- "
1476
- class Foo
1477
- def bar(x, y); end
1478
- end
1479
- ",
1480
- );
1481
- context.resolve();
1482
-
1483
- let name_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
1484
- assert_declaration_completion_eq!(
1485
- context,
1486
- CompletionReceiver::MethodArgument {
1487
- self_name_id: name_id,
1488
- method_decl_id: DeclarationId::from("Foo#bar()"),
1489
- },
1490
- ["Foo", "Foo#bar()"]
1491
- );
1492
- }
1493
-
1494
- #[test]
1495
- fn method_argument_completion_mixed_params() {
1496
- let mut context = GraphTest::new();
1497
-
1498
- context.index_uri(
1499
- "file:///foo.rb",
1500
- "
1501
- class Foo
1502
- def search(query, limit:, offset: 0, **opts); end
1503
- end
1504
- ",
1505
- );
1506
- context.resolve();
1507
-
1508
- let name_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
1509
- assert_declaration_completion_eq!(
1510
- context,
1511
- CompletionReceiver::MethodArgument {
1512
- self_name_id: name_id,
1513
- method_decl_id: DeclarationId::from("Foo#search()"),
1514
- },
1515
- // Only RequiredKeyword and OptionalKeyword, not RestKeyword (**opts)
1516
- ["Foo", "Foo#search()", "limit:", "offset:"]
1517
- );
1518
- }
1519
-
1520
- #[test]
1521
- fn first_entry_is_always_used_overridden_methods() {
1522
- let mut context = GraphTest::new();
1523
- context.index_uri(
1524
- "file:///foo.rb",
1525
- "
1526
- class Foo
1527
- def bar(first:, second:); end
1528
- end
1529
- ",
1530
- );
1531
- context.index_uri(
1532
- "file:///foo2.rb",
1533
- "
1534
- class Foo
1535
- def bar(first:); end
1536
- end
1537
- ",
1538
- );
1539
- context.resolve();
1540
-
1541
- let name_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
1542
- assert_declaration_completion_eq!(
1543
- context,
1544
- CompletionReceiver::MethodArgument {
1545
- self_name_id: name_id,
1546
- method_decl_id: DeclarationId::from("Foo#bar()"),
1547
- },
1548
- ["Foo", "Foo#bar()", "first:", "second:"]
1549
- );
1550
- }
1551
-
1552
- #[test]
1553
- fn expression_completion_includes_keywords() {
1554
- let mut context = GraphTest::new();
1555
- context.index_uri("file:///foo.rb", "class Foo; end");
1556
- context.resolve();
1557
-
1558
- let name_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
1559
- assert_completion_eq!(
1560
- context,
1561
- CompletionReceiver::Expression(name_id),
1562
- [
1563
- "Foo",
1564
- "BEGIN",
1565
- "END",
1566
- "__ENCODING__",
1567
- "__FILE__",
1568
- "__LINE__",
1569
- "alias",
1570
- "and",
1571
- "begin",
1572
- "break",
1573
- "case",
1574
- "class",
1575
- "def",
1576
- "defined?",
1577
- "do",
1578
- "else",
1579
- "elsif",
1580
- "end",
1581
- "ensure",
1582
- "false",
1583
- "for",
1584
- "if",
1585
- "in",
1586
- "module",
1587
- "next",
1588
- "nil",
1589
- "not",
1590
- "or",
1591
- "redo",
1592
- "rescue",
1593
- "retry",
1594
- "return",
1595
- "self",
1596
- "super",
1597
- "then",
1598
- "true",
1599
- "undef",
1600
- "unless",
1601
- "until",
1602
- "when",
1603
- "while",
1604
- "yield",
1605
- ]
1606
- );
1607
- }
1608
-
1609
- #[test]
1610
- fn method_argument_completion_includes_keywords() {
1611
- let mut context = GraphTest::new();
1612
- context.index_uri("file:///foo.rb", "class Foo; def bar(name:); end; end");
1613
- context.resolve();
1614
-
1615
- let name_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
1616
- assert_completion_eq!(
1617
- context,
1618
- CompletionReceiver::MethodArgument {
1619
- self_name_id: name_id,
1620
- method_decl_id: DeclarationId::from("Foo#bar()"),
1621
- },
1622
- [
1623
- "Foo",
1624
- "Foo#bar()",
1625
- "BEGIN",
1626
- "END",
1627
- "__ENCODING__",
1628
- "__FILE__",
1629
- "__LINE__",
1630
- "alias",
1631
- "and",
1632
- "begin",
1633
- "break",
1634
- "case",
1635
- "class",
1636
- "def",
1637
- "defined?",
1638
- "do",
1639
- "else",
1640
- "elsif",
1641
- "end",
1642
- "ensure",
1643
- "false",
1644
- "for",
1645
- "if",
1646
- "in",
1647
- "module",
1648
- "next",
1649
- "nil",
1650
- "not",
1651
- "or",
1652
- "redo",
1653
- "rescue",
1654
- "retry",
1655
- "return",
1656
- "self",
1657
- "super",
1658
- "then",
1659
- "true",
1660
- "undef",
1661
- "unless",
1662
- "until",
1663
- "when",
1664
- "while",
1665
- "yield",
1666
- "name:",
1667
- ]
1668
- );
1669
- }
1670
-
1671
- #[test]
1672
- fn namespace_access_completion_excludes_keywords() {
1673
- let mut context = GraphTest::new();
1674
- context.index_uri("file:///foo.rb", "class Foo; CONST = 1; end");
1675
- context.resolve();
1676
-
1677
- let candidates = completion_candidates(
1678
- context.graph(),
1679
- CompletionContext::new(CompletionReceiver::NamespaceAccess(DeclarationId::from("Foo"))),
1680
- )
1681
- .unwrap();
1682
-
1683
- assert!(!candidates.iter().any(|c| matches!(c, CompletionCandidate::Keyword(_))));
1684
- }
1685
-
1686
- #[test]
1687
- fn method_call_completion_excludes_keywords() {
1688
- let mut context = GraphTest::new();
1689
- context.index_uri("file:///foo.rb", "class Foo; def bar; end; end");
1690
- context.resolve();
1691
-
1692
- let candidates = completion_candidates(
1693
- context.graph(),
1694
- CompletionContext::new(CompletionReceiver::MethodCall(DeclarationId::from("Foo"))),
1695
- )
1696
- .unwrap();
1697
-
1698
- assert!(!candidates.iter().any(|c| matches!(c, CompletionCandidate::Keyword(_))));
1699
- }
1700
- }
1
+ use std::collections::HashSet;
2
+ use std::error::Error;
3
+ use std::path::PathBuf;
4
+ use std::thread;
5
+
6
+ use url::Url;
7
+
8
+ use crate::model::built_in::OBJECT_ID;
9
+ use crate::model::declaration::{Ancestor, Declaration};
10
+ use crate::model::definitions::{Definition, Parameter};
11
+ use crate::model::graph::Graph;
12
+ use crate::model::identity_maps::IdentityHashSet;
13
+ use crate::model::ids::{DeclarationId, NameId, StringId, UriId};
14
+ use crate::model::keywords::{self, Keyword};
15
+ use crate::model::name::NameRef;
16
+
17
+ /// Controls how declaration names are matched against the search query.
18
+ #[derive(Default)]
19
+ pub enum MatchMode {
20
+ /// Fuzzy matching: query characters must appear in order in the target (case-insensitive). Used for LSP workspace
21
+ /// symbol.
22
+ #[default]
23
+ Fuzzy,
24
+ /// Exact partial matching: query must appear as a contiguous substring of the target. Used for precise filtering
25
+ /// (e.g., finding all declarations containing `#is_a?()`).
26
+ Exact,
27
+ }
28
+
29
+ /// # Panics
30
+ ///
31
+ /// Will panic if any of the threads panic
32
+ pub fn declaration_search(graph: &Graph, query: &str, match_mode: &MatchMode) -> Vec<DeclarationId> {
33
+ let num_threads = thread::available_parallelism().map_or(4, std::num::NonZero::get);
34
+ let declarations = graph.declarations();
35
+ let ids: Vec<DeclarationId> = declarations.keys().copied().collect();
36
+ let chunk_size = ids.len().div_ceil(num_threads);
37
+
38
+ if chunk_size == 0 {
39
+ return Vec::new();
40
+ }
41
+
42
+ thread::scope(|s| {
43
+ let handles: Vec<_> = ids
44
+ .chunks(chunk_size)
45
+ .map(|chunk| {
46
+ s.spawn(|| {
47
+ chunk
48
+ .iter()
49
+ .filter(|id| {
50
+ let declaration = declarations.get(id).unwrap();
51
+ let name = declaration.name();
52
+ match match_mode {
53
+ MatchMode::Fuzzy => {
54
+ // When the query is empty, we return everything as per the LSP specification.
55
+ // Otherwise, we compute the match score and return anything with a score greater than zero
56
+ query.is_empty() || match_score(query, name) > 0
57
+ }
58
+ MatchMode::Exact => name.contains(query),
59
+ }
60
+ })
61
+ .copied()
62
+ .collect::<Vec<_>>()
63
+ })
64
+ })
65
+ .collect();
66
+
67
+ handles.into_iter().flat_map(|h| h.join().unwrap()).collect()
68
+ })
69
+ }
70
+
71
+ #[must_use]
72
+ fn match_score(query: &str, target: &str) -> usize {
73
+ let mut query_chars = query.chars().peekable();
74
+ let mut score = 0;
75
+
76
+ // Count the number of matches in the order of the query, so that character ordering is taken into account
77
+ for t_char in target.chars() {
78
+ if let Some(&q_char) = query_chars.peek()
79
+ && q_char.eq_ignore_ascii_case(&t_char)
80
+ {
81
+ score += 1;
82
+ query_chars.next();
83
+ }
84
+ }
85
+
86
+ // If after going through the target, there are still query characters left, then some of the query can't be found
87
+ // in this target and we return zero to indicate a non-match
88
+ if query_chars.peek().is_some() { 0 } else { score }
89
+ }
90
+
91
+ /// Resolves a require path to its URI ID. Used for go-to-definition.
92
+ ///
93
+ /// Searches the `load_path` in order and returns the first match, mirroring how Ruby's `require`
94
+ /// walks `$LOAD_PATH`.
95
+ #[must_use]
96
+ pub fn resolve_require_path(graph: &Graph, require_path: &str, load_path: &[PathBuf]) -> Option<UriId> {
97
+ let normalized = require_path.trim_end_matches(".rb");
98
+
99
+ for path in load_path {
100
+ let file_path = path.join(format!("{normalized}.rb"));
101
+ let Ok(url) = Url::from_file_path(&file_path) else {
102
+ continue;
103
+ };
104
+ let uri_id = UriId::from(url.as_str());
105
+ if graph.documents().contains_key(&uri_id) {
106
+ return Some(uri_id);
107
+ }
108
+ }
109
+
110
+ None
111
+ }
112
+
113
+ /// Returns all require paths. Used for completion.
114
+ ///
115
+ /// When multiple files resolve to the same require path (e.g., `foo.rb` exists in multiple
116
+ /// load paths), the one from the earliest load path wins. This matches Ruby's `require` behavior.
117
+ ///
118
+ /// # Panics
119
+ ///
120
+ /// Panics if one of the search threads panics
121
+ #[must_use]
122
+ pub fn require_paths(graph: &Graph, load_paths: &[PathBuf]) -> Vec<String> {
123
+ let num_threads = thread::available_parallelism().map_or(4, std::num::NonZero::get);
124
+ let documents = graph.documents().iter().collect::<Vec<_>>();
125
+ let chunk_size = documents.len().div_ceil(num_threads);
126
+
127
+ if chunk_size == 0 {
128
+ return Vec::new();
129
+ }
130
+
131
+ let mut all_results = thread::scope(|scope| {
132
+ let handles: Vec<_> = documents
133
+ .chunks(chunk_size)
134
+ .map(|chunk| {
135
+ scope.spawn(move || {
136
+ chunk
137
+ .iter()
138
+ .filter_map(|(_, document)| document.require_path(load_paths))
139
+ .collect::<Vec<_>>()
140
+ })
141
+ })
142
+ .collect();
143
+
144
+ handles
145
+ .into_iter()
146
+ .flat_map(|handle| handle.join().unwrap())
147
+ .collect::<Vec<_>>()
148
+ });
149
+
150
+ // Sort by load path index so earlier load paths win during deduplication
151
+ all_results.sort_by_key(|(_, index)| *index);
152
+
153
+ let mut seen = HashSet::new();
154
+ all_results
155
+ .into_iter()
156
+ .filter(|(require_path, _)| seen.insert(require_path.clone()))
157
+ .map(|(require_path, _)| require_path)
158
+ .collect()
159
+ }
160
+
161
+ /// A completion candidate
162
+ pub enum CompletionCandidate {
163
+ Declaration(DeclarationId),
164
+ KeywordArgument(StringId),
165
+ Keyword(&'static Keyword),
166
+ }
167
+
168
+ /// The context in which completion is being requested
169
+ pub enum CompletionReceiver {
170
+ /// Completion requested for an expression with no previous token (e.g.: at the start of a line with nothing before)
171
+ /// Includes: all keywords, all global variables and reacheable instance variables, class variables, constants and methods
172
+ Expression(NameId),
173
+ /// Completion requested after a namespace access operator (e.g.: `Foo::`)
174
+ /// Includes: all constants and singleton methods for the namespace and its ancestors
175
+ NamespaceAccess(DeclarationId),
176
+ /// Completion requested after a method call operator (e.g.: `foo.`, `@bar.`, `@@baz.`, `Qux.`).
177
+ /// In the case of singleton completion (e.g.: `Foo.`), the declaration ID should be for the singleton class (i.e.: `Foo::<Foo>`)
178
+ /// Includes: all methods that exist on the type of the receiver and its ancestors
179
+ MethodCall(DeclarationId),
180
+ /// Completion requested inside a method call's argument list (e.g.: `foo.bar(|)`)
181
+ /// Includes: everything expressions do plus keyword parameter names of the method being called
182
+ MethodArgument {
183
+ self_name_id: NameId,
184
+ method_decl_id: DeclarationId,
185
+ },
186
+ }
187
+
188
+ pub struct CompletionContext<'a> {
189
+ seen_members: IdentityHashSet<&'a StringId>,
190
+ completion_receiver: CompletionReceiver,
191
+ }
192
+
193
+ impl<'a> CompletionContext<'a> {
194
+ #[must_use]
195
+ pub fn new(completion_receiver: CompletionReceiver) -> Self {
196
+ Self {
197
+ seen_members: IdentityHashSet::default(),
198
+ completion_receiver,
199
+ }
200
+ }
201
+
202
+ pub fn dedup(&mut self, member_str_id: &'a StringId) -> bool {
203
+ self.seen_members.insert(member_str_id)
204
+ }
205
+ }
206
+
207
+ /// Collects completion candidate members
208
+ macro_rules! collect_candidates {
209
+ // Collect all members with no filtering
210
+ ($declaration:expr, $context:expr, $candidates:expr) => {
211
+ for (member_str_id, member_declaration_id) in $declaration.members() {
212
+ if $context.dedup(member_str_id) {
213
+ $candidates.push(CompletionCandidate::Declaration(*member_declaration_id));
214
+ }
215
+ }
216
+ };
217
+ // Collect only members matching certain kinds
218
+ ($graph:expr, $declaration:expr, $context:expr, $candidates:expr, $kinds:pat) => {
219
+ for (member_str_id, member_declaration_id) in $declaration.members() {
220
+ let member = $graph.declarations().get(member_declaration_id).unwrap();
221
+
222
+ if matches!(member, $kinds) && $context.dedup(member_str_id) {
223
+ $candidates.push(CompletionCandidate::Declaration(*member_declaration_id));
224
+ }
225
+ }
226
+ };
227
+ }
228
+
229
+ /// Determines all possible completion candidates based on the current context of the cursor. There are multiple cases
230
+ /// that change what has to be collected for completion:
231
+ ///
232
+ /// - Expressions collect all keywords, constants, methods, instance variables, class variables, local variables and
233
+ /// global variables that are reacheable from the current lexical scope and self type
234
+ /// - Expression in method arguments collects everything that expressions do and all keyword parameter names that are
235
+ /// applicable to the method being called
236
+ /// everything else
237
+ /// - Namespace access (e.g.: `Foo::`) collects all constants and singleton methods for the namespace that `Foo`
238
+ /// resolves to
239
+ /// - Method calls on anything (e.g.: `foo.`, `@bar.`, `@@baz.`, `Qux.`) collects all methods that exist on the type
240
+ /// returned by the receiver
241
+ ///
242
+ /// # Panics
243
+ ///
244
+ /// Will panic if we incorrectly inserted non namespace declarations as ancestors
245
+ ///
246
+ /// # Errors
247
+ ///
248
+ /// Will error if the given `self_name_id` does not point to a namespace declaration
249
+ pub fn completion_candidates<'a>(
250
+ graph: &'a Graph,
251
+ context: CompletionContext<'a>,
252
+ ) -> Result<Vec<CompletionCandidate>, Box<dyn Error>> {
253
+ match context.completion_receiver {
254
+ CompletionReceiver::Expression(self_name_id) => expression_completion(graph, self_name_id, context),
255
+ CompletionReceiver::NamespaceAccess(decl_id) => namespace_access_completion(graph, decl_id, context),
256
+ CompletionReceiver::MethodCall(decl_id) => method_call_completion(graph, decl_id, context),
257
+ CompletionReceiver::MethodArgument {
258
+ self_name_id,
259
+ method_decl_id,
260
+ } => method_argument_completion(graph, self_name_id, method_decl_id, context),
261
+ }
262
+ }
263
+
264
+ /// Resolves a declaration ID to a namespace, following constant aliases if necessary.
265
+ ///
266
+ /// Returns:
267
+ /// - `Ok(Some(id))` if the declaration is a namespace (directly or via alias)
268
+ /// - `Ok(None)` if the declaration does not exist in the graph
269
+ /// - `Err(...)` if the declaration exists but is not a namespace or alias to a namespace
270
+ fn resolve_to_namespace(graph: &Graph, decl_id: DeclarationId) -> Result<Option<DeclarationId>, Box<dyn Error>> {
271
+ match graph.declarations().get(&decl_id) {
272
+ Some(Declaration::Namespace(_)) => Ok(Some(decl_id)),
273
+ None => Ok(None),
274
+ Some(_) => {
275
+ if let Some(target_id) = graph.resolve_alias(&decl_id)
276
+ && let Some(Declaration::Namespace(_)) = graph.declarations().get(&target_id)
277
+ {
278
+ Ok(Some(target_id))
279
+ } else {
280
+ Err(format!("Expected declaration {decl_id:?} to be a namespace or alias to a namespace").into())
281
+ }
282
+ }
283
+ }
284
+ }
285
+
286
+ /// Collect completion for a namespace access (e.g.: `Foo::`)
287
+ fn namespace_access_completion<'a>(
288
+ graph: &'a Graph,
289
+ namespace_decl_id: DeclarationId,
290
+ mut context: CompletionContext<'a>,
291
+ ) -> Result<Vec<CompletionCandidate>, Box<dyn Error>> {
292
+ let Some(resolved_id) = resolve_to_namespace(graph, namespace_decl_id)? else {
293
+ return Ok(Vec::new());
294
+ };
295
+ let namespace = graph.declarations().get(&resolved_id).unwrap().as_namespace().unwrap();
296
+ let mut candidates = Vec::new();
297
+
298
+ // Walk ancestors collecting inherited constants, stopping at Object to avoid surfacing top-level constants
299
+ // from Object, Kernel, BasicObject, etc.
300
+ for ancestor in namespace.ancestors() {
301
+ if let Ancestor::Complete(ancestor_id) = ancestor {
302
+ // Do not offer completion for constants inherited after `Object` (e.g.: `Object::String`). While this is
303
+ // valid Ruby code, it's extremely uncommon and not a super valuable completion suggestion
304
+ if *ancestor_id == *OBJECT_ID {
305
+ break;
306
+ }
307
+
308
+ let ancestor_decl = graph.declarations().get(ancestor_id).unwrap().as_namespace().unwrap();
309
+
310
+ collect_candidates!(
311
+ graph,
312
+ &ancestor_decl,
313
+ context,
314
+ candidates,
315
+ Declaration::Namespace(_) | Declaration::Constant(_) | Declaration::ConstantAlias(_)
316
+ );
317
+ }
318
+ }
319
+
320
+ // Collect singleton methods from the singleton class and its ancestors
321
+ if let Some(singleton_id) = namespace.singleton_class() {
322
+ let singleton = graph.declarations().get(singleton_id).unwrap().as_namespace().unwrap();
323
+
324
+ for ancestor in singleton.ancestors() {
325
+ if let Ancestor::Complete(ancestor_id) = ancestor {
326
+ let ancestor_decl = graph.declarations().get(ancestor_id).unwrap().as_namespace().unwrap();
327
+ collect_candidates!(graph, &ancestor_decl, context, candidates, Declaration::Method(_));
328
+ }
329
+ }
330
+ }
331
+
332
+ Ok(candidates)
333
+ }
334
+
335
+ /// Collect completion for a method call (e.g.: `foo.`, `@bar.`, `Baz.`)
336
+ fn method_call_completion<'a>(
337
+ graph: &'a Graph,
338
+ receiver_decl_id: DeclarationId,
339
+ mut context: CompletionContext<'a>,
340
+ ) -> Result<Vec<CompletionCandidate>, Box<dyn Error>> {
341
+ let Some(resolved_id) = resolve_to_namespace(graph, receiver_decl_id)? else {
342
+ return Ok(Vec::new());
343
+ };
344
+ let namespace = graph.declarations().get(&resolved_id).unwrap().as_namespace().unwrap();
345
+ let mut candidates = Vec::new();
346
+
347
+ for ancestor in namespace.ancestors() {
348
+ if let Ancestor::Complete(ancestor_id) = ancestor {
349
+ let ancestor_decl = graph.declarations().get(ancestor_id).unwrap().as_namespace().unwrap();
350
+ collect_candidates!(graph, &ancestor_decl, context, candidates, Declaration::Method(_));
351
+ }
352
+ }
353
+
354
+ Ok(candidates)
355
+ }
356
+
357
+ /// Collect completion for an expression
358
+ fn expression_completion<'a>(
359
+ graph: &'a Graph,
360
+ self_name_id: NameId,
361
+ mut context: CompletionContext<'a>,
362
+ ) -> Result<Vec<CompletionCandidate>, Box<dyn Error>> {
363
+ let Some(name_ref) = graph.names().get(&self_name_id) else {
364
+ return Err(format!("Name {self_name_id} not found in graph").into());
365
+ };
366
+ let NameRef::Resolved(name_ref) = name_ref else {
367
+ return Err(format!("Expected name {self_name_id} to be resolved").into());
368
+ };
369
+ let Some(self_decl) = graph
370
+ .declarations()
371
+ .get(name_ref.declaration_id())
372
+ .and_then(|d| d.as_namespace())
373
+ else {
374
+ return Err("Expected associated declaration to be a namespace".into());
375
+ };
376
+ let mut candidates = Vec::new();
377
+
378
+ // Walk the name's lexical scopes, collecting all constant completion members
379
+ let mut current_name_id = Some(self_name_id);
380
+
381
+ while let Some(id) = current_name_id {
382
+ let NameRef::Resolved(name_ref) = graph.names().get(&id).unwrap() else {
383
+ break;
384
+ };
385
+
386
+ let nesting_decl = graph
387
+ .declarations()
388
+ .get(name_ref.declaration_id())
389
+ .unwrap()
390
+ .as_namespace()
391
+ .unwrap();
392
+
393
+ collect_candidates!(
394
+ graph,
395
+ &nesting_decl,
396
+ context,
397
+ candidates,
398
+ Declaration::Namespace(_) | Declaration::Constant(_) | Declaration::ConstantAlias(_)
399
+ );
400
+
401
+ current_name_id = *name_ref.nesting();
402
+ }
403
+
404
+ // Include all top level constants and globals, which are accessible everywhere
405
+ let object = graph.declarations().get(&OBJECT_ID).unwrap().as_namespace().unwrap();
406
+ collect_candidates!(
407
+ graph,
408
+ &object,
409
+ context,
410
+ candidates,
411
+ Declaration::Namespace(_)
412
+ | Declaration::Constant(_)
413
+ | Declaration::ConstantAlias(_)
414
+ | Declaration::GlobalVariable(_)
415
+ );
416
+
417
+ // Walk ancestors collecting all applicable completion members
418
+ for ancestor in self_decl.ancestors() {
419
+ if let Ancestor::Complete(ancestor_id) = ancestor {
420
+ let ancestor_decl = graph.declarations().get(ancestor_id).unwrap().as_namespace().unwrap();
421
+ collect_candidates!(&ancestor_decl, context, candidates);
422
+
423
+ // Collect class variables from the attached object, which are available at any singleton class level
424
+ // within self
425
+ let attached_object = graph.attached_object(ancestor_decl);
426
+ collect_candidates!(
427
+ graph,
428
+ &attached_object,
429
+ context,
430
+ candidates,
431
+ Declaration::ClassVariable(_)
432
+ );
433
+ }
434
+ }
435
+
436
+ // Keywords are always available in expression contexts
437
+ candidates.extend(keywords::KEYWORDS.iter().map(CompletionCandidate::Keyword));
438
+ Ok(candidates)
439
+ }
440
+
441
+ /// Collect completion for a method argument (e.g.: `foo.bar(|)`)
442
+ fn method_argument_completion<'a>(
443
+ graph: &'a Graph,
444
+ self_name_id: NameId,
445
+ method_decl_id: DeclarationId,
446
+ context: CompletionContext<'a>,
447
+ ) -> Result<Vec<CompletionCandidate>, Box<dyn Error>> {
448
+ let mut candidates = expression_completion(graph, self_name_id, context)?;
449
+ let Some(method_decl) = graph.declarations().get(&method_decl_id) else {
450
+ return Ok(candidates);
451
+ };
452
+
453
+ // Find the first Method definition to extract keyword parameters
454
+ for def_id in method_decl.definitions() {
455
+ if let Some(Definition::Method(method_def)) = graph.definitions().get(def_id) {
456
+ for signature in method_def.signatures().as_slice() {
457
+ for param in signature {
458
+ match param {
459
+ Parameter::RequiredKeyword(p) | Parameter::OptionalKeyword(p) => {
460
+ candidates.push(CompletionCandidate::KeywordArgument(*p.str()));
461
+ }
462
+ _ => {}
463
+ }
464
+ }
465
+ }
466
+ break;
467
+ }
468
+ }
469
+
470
+ Ok(candidates)
471
+ }
472
+
473
+ #[cfg(test)]
474
+ mod tests {
475
+ use std::str::FromStr;
476
+ use url::Url;
477
+
478
+ use super::*;
479
+ use crate::{
480
+ model::{
481
+ ids::StringId,
482
+ name::{Name, ParentScope},
483
+ },
484
+ test_utils::GraphTest,
485
+ };
486
+
487
+ macro_rules! assert_results_eq {
488
+ ($context:expr, $query:expr, $expected:expr) => {
489
+ assert_results_eq!($context, $query, &MatchMode::default(), $expected);
490
+ };
491
+ ($context:expr, $query:expr, $match_mode:expr, $expected:expr) => {
492
+ let actual = declaration_search(&$context.graph(), $query, $match_mode);
493
+ assert_eq!(
494
+ actual,
495
+ $expected
496
+ .into_iter()
497
+ .map(|s| DeclarationId::from(s))
498
+ .collect::<Vec<DeclarationId>>(),
499
+ "Unexpected search results: {:?}",
500
+ actual
501
+ .iter()
502
+ .map(|id| $context
503
+ .graph()
504
+ .declarations()
505
+ .get(id)
506
+ .unwrap()
507
+ .name()
508
+ .to_string())
509
+ .collect::<Vec<String>>()
510
+ );
511
+ };
512
+ }
513
+
514
+ fn candidate_label(context: &GraphTest, candidate: &CompletionCandidate) -> String {
515
+ match candidate {
516
+ CompletionCandidate::Declaration(id) => context.graph().declarations().get(id).unwrap().name().to_string(),
517
+ CompletionCandidate::KeywordArgument(str_id) => {
518
+ format!("{}:", context.graph().strings().get(str_id).unwrap().as_str())
519
+ }
520
+ CompletionCandidate::Keyword(kw) => kw.name().to_string(),
521
+ }
522
+ }
523
+
524
+ macro_rules! assert_completion_eq {
525
+ ($context:expr, $receiver:expr, $expected:expr) => {
526
+ assert_eq!(
527
+ $expected,
528
+ *completion_candidates($context.graph(), CompletionContext::new($receiver))
529
+ .unwrap()
530
+ .iter()
531
+ .map(|candidate| candidate_label(&$context, candidate))
532
+ .collect::<Vec<_>>()
533
+ );
534
+ };
535
+ }
536
+
537
+ /// Asserts declaration and keyword argument completion candidates, excluding language keywords.
538
+ /// Language keywords are always present in expression contexts and tested separately.
539
+ macro_rules! assert_declaration_completion_eq {
540
+ ($context:expr, $receiver:expr, $expected:expr) => {
541
+ assert_eq!(
542
+ $expected,
543
+ *completion_candidates($context.graph(), CompletionContext::new($receiver))
544
+ .unwrap()
545
+ .iter()
546
+ .filter(|c| !matches!(c, CompletionCandidate::Keyword(_)))
547
+ .map(|candidate| candidate_label(&$context, candidate))
548
+ .collect::<Vec<_>>()
549
+ );
550
+ };
551
+ }
552
+
553
+ #[test]
554
+ fn fuzzy_search_returns_partial_matches() {
555
+ let mut context = GraphTest::new();
556
+ context.index_uri("file:///foo.rb", {
557
+ r"
558
+ class Foo
559
+ end
560
+ "
561
+ });
562
+ context.resolve();
563
+ assert_results_eq!(context, "Fo", ["Foo"]);
564
+ }
565
+
566
+ #[test]
567
+ fn exact_partial_match_search() {
568
+ let mut context = GraphTest::new();
569
+ context.index_uri("file:///foo.rb", {
570
+ r"
571
+ class Foo
572
+ def is_a_foo?; end
573
+ end
574
+
575
+ class Bar < Foo
576
+ def is_a?(other); end
577
+ end
578
+ "
579
+ });
580
+ context.resolve();
581
+ assert_results_eq!(context, "#is_a?()", &MatchMode::Exact, ["Bar#is_a?()"]);
582
+ }
583
+
584
+ #[test]
585
+ fn exact_match_empty_query_returns_all() {
586
+ let mut context = GraphTest::new();
587
+ context.index_uri("file:///foo.rb", {
588
+ r"
589
+ class Foo; end
590
+ class Bar; end
591
+ "
592
+ });
593
+ context.resolve();
594
+ let exact_results = declaration_search(context.graph(), "", &MatchMode::Exact);
595
+ let fuzzy_results = declaration_search(context.graph(), "", &MatchMode::Fuzzy);
596
+
597
+ assert_eq!(exact_results.len(), fuzzy_results.len());
598
+ assert_eq!(context.graph().declarations().len(), exact_results.len());
599
+ }
600
+
601
+ #[test]
602
+ fn exact_match_is_case_sensitive() {
603
+ let mut context = GraphTest::new();
604
+ context.index_uri("file:///foo.rb", {
605
+ r"
606
+ class Foo
607
+ def is_a_foo?; end
608
+ end
609
+
610
+ class Bar < Foo
611
+ def is_a?(other); end
612
+ end
613
+ "
614
+ });
615
+ context.resolve();
616
+
617
+ assert_results_eq!(context, "#Is_A?()", &MatchMode::Exact, Vec::<&str>::new());
618
+ assert_results_eq!(context, "#Is_A?()", ["Foo#is_a_foo?()", "Bar#is_a?()"]);
619
+ }
620
+
621
+ fn test_root() -> PathBuf {
622
+ let root = if cfg!(windows) { "C:\\" } else { "/" };
623
+ PathBuf::from_str(root).unwrap()
624
+ }
625
+
626
+ #[test]
627
+ fn test_resolve_require_path() {
628
+ let root = test_root();
629
+ let path = root
630
+ .join("lib")
631
+ .join("foo")
632
+ .join("bar.rb")
633
+ .to_str()
634
+ .unwrap()
635
+ .to_string();
636
+ let uri = Url::from_file_path(path).unwrap().to_string();
637
+ let load_paths = [root.join("lib")];
638
+
639
+ let mut context = GraphTest::new();
640
+ context.index_uri(&uri, "class Bar; end");
641
+
642
+ // finds basic path
643
+ let uri_id = resolve_require_path(context.graph(), "foo/bar", &load_paths);
644
+ assert!(uri_id.is_some());
645
+ let doc = context.graph().documents().get(&uri_id.unwrap()).unwrap();
646
+ assert_eq!(uri, doc.uri());
647
+
648
+ // handles .rb suffix
649
+ let uri_id_with_rb = resolve_require_path(context.graph(), "foo/bar.rb", &load_paths);
650
+ assert_eq!(uri_id, uri_id_with_rb);
651
+
652
+ // returns None for nonexistent
653
+ assert!(resolve_require_path(context.graph(), "nonexistent", &load_paths).is_none());
654
+ }
655
+
656
+ #[test]
657
+ fn test_resolve_require_path_prefers_earliest_load_path() {
658
+ let root = test_root();
659
+ let lib_path = root.join("lib").join("foo").join("bar.rb");
660
+ let test_path = root.join("test").join("foo").join("bar.rb");
661
+ let lib_uri = Url::from_file_path(&lib_path).unwrap().to_string();
662
+ let test_uri = Url::from_file_path(&test_path).unwrap().to_string();
663
+
664
+ let mut context = GraphTest::new();
665
+ context.index_uri(&lib_uri, "class Bar; end");
666
+ context.index_uri(&test_uri, "class Bar; end");
667
+
668
+ // lib comes first in load paths
669
+ let load_paths = [root.join("lib"), root.join("test")];
670
+ let uri_id = resolve_require_path(context.graph(), "foo/bar", &load_paths).unwrap();
671
+ let doc = context.graph().documents().get(&uri_id).unwrap();
672
+ assert!(
673
+ doc.uri().contains("lib/foo/bar.rb"),
674
+ "Expected lib path, got {}",
675
+ doc.uri()
676
+ );
677
+
678
+ // test comes first in load paths
679
+ let load_paths = [root.join("test"), root.join("lib")];
680
+ let uri_id = resolve_require_path(context.graph(), "foo/bar", &load_paths).unwrap();
681
+ let doc = context.graph().documents().get(&uri_id).unwrap();
682
+ assert!(
683
+ doc.uri().contains("test/foo/bar.rb"),
684
+ "Expected test path, got {}",
685
+ doc.uri()
686
+ );
687
+ }
688
+
689
+ #[test]
690
+ fn test_require_paths() {
691
+ let root = test_root();
692
+ let path_bar = root.join("lib").join("foo").join("bar.rb");
693
+ let path_qux = root.join("lib").join("foo").join("qux.rb");
694
+ let path_foobar = root.join("lib").join("foobar.rb");
695
+ let uri_bar = Url::from_file_path(&path_bar).unwrap().to_string();
696
+ let uri_qux = Url::from_file_path(&path_qux).unwrap().to_string();
697
+ let uri_foobar = Url::from_file_path(&path_foobar).unwrap().to_string();
698
+ let load_paths = vec![root.join("lib")];
699
+
700
+ let mut context = GraphTest::new();
701
+ context.index_uri(&uri_bar, "class Bar; end");
702
+ context.index_uri(&uri_qux, "class Qux; end");
703
+ context.index_uri(&uri_foobar, "class Foobar; end");
704
+
705
+ let results = require_paths(context.graph(), &load_paths);
706
+
707
+ assert_eq!(3, results.len());
708
+ assert!(results.contains(&"foo/bar".to_string()));
709
+ assert!(results.contains(&"foo/qux".to_string()));
710
+ assert!(results.contains(&"foobar".to_string()));
711
+ }
712
+
713
+ #[test]
714
+ fn test_require_paths_deduplicates_by_load_path_order() {
715
+ let root = test_root();
716
+ let path1 = root.join("lib1").join("foo.rb");
717
+ let path2 = root.join("lib2").join("foo.rb");
718
+ let uri1 = Url::from_file_path(&path1).unwrap().to_string();
719
+ let uri2 = Url::from_file_path(&path2).unwrap().to_string();
720
+ let load_paths = [root.join("lib1"), root.join("lib2")];
721
+
722
+ let mut context = GraphTest::new();
723
+ context.index_uri(&uri1, "class Foo; end");
724
+ context.index_uri(&uri2, "class Foo; end");
725
+
726
+ let results = require_paths(context.graph(), &load_paths);
727
+
728
+ let foo_count = results.iter().filter(|p| *p == "foo").count();
729
+ assert_eq!(1, foo_count);
730
+ }
731
+
732
+ #[test]
733
+ fn completion_candidates_on_self() {
734
+ let mut context = GraphTest::new();
735
+
736
+ context.index_uri(
737
+ "file:///foo.rb",
738
+ "
739
+ module Foo
740
+ CONST = 1
741
+ def bar; end
742
+ end
743
+
744
+ class Parent
745
+ def initialize
746
+ @var = 1
747
+ end
748
+ end
749
+
750
+ class Child < Parent
751
+ include Foo
752
+
753
+ def baz
754
+ # Completion in this `self` context
755
+ end
756
+ end
757
+ ",
758
+ );
759
+ context.resolve();
760
+
761
+ let name_id = Name::new(StringId::from("Child"), ParentScope::None, None).id();
762
+ assert_declaration_completion_eq!(
763
+ context,
764
+ CompletionReceiver::Expression(name_id),
765
+ [
766
+ "Class",
767
+ "BasicObject",
768
+ "Child",
769
+ "Parent",
770
+ "Kernel",
771
+ "Module",
772
+ "Foo",
773
+ "Object",
774
+ "Child#baz()",
775
+ "Foo::CONST",
776
+ "Foo#bar()",
777
+ "Parent#initialize()",
778
+ "Parent#@var"
779
+ ]
780
+ );
781
+ }
782
+
783
+ #[test]
784
+ fn completion_candidates_shows_first_option_in_the_ancestor_chain() {
785
+ let mut context = GraphTest::new();
786
+
787
+ context.index_uri(
788
+ "file:///foo.rb",
789
+ "
790
+ module Foo
791
+ def bar; end
792
+ end
793
+
794
+ class Parent
795
+ def bar; end
796
+ end
797
+
798
+ class Child < Parent
799
+ def bar
800
+ # Completion in this `self` context
801
+ end
802
+ end
803
+ ",
804
+ );
805
+ context.resolve();
806
+
807
+ let name_id = Name::new(StringId::from("Child"), ParentScope::None, None).id();
808
+ assert_declaration_completion_eq!(
809
+ context,
810
+ CompletionReceiver::Expression(name_id),
811
+ [
812
+ "Class",
813
+ "BasicObject",
814
+ "Child",
815
+ "Parent",
816
+ "Kernel",
817
+ "Module",
818
+ "Foo",
819
+ "Object",
820
+ "Child#bar()"
821
+ ]
822
+ );
823
+ }
824
+
825
+ #[test]
826
+ fn completion_candidates_in_a_cyclic_ancestor_chain() {
827
+ let mut context = GraphTest::new();
828
+
829
+ context.index_uri(
830
+ "file:///foo.rb",
831
+ "
832
+ module Foo
833
+ include Baz
834
+
835
+ def foo_m; end
836
+ end
837
+
838
+ module Bar
839
+ include Foo
840
+
841
+ def bar_m; end
842
+ end
843
+
844
+ module Baz
845
+ include Bar
846
+
847
+ def baz_m; end
848
+ end
849
+ ",
850
+ );
851
+ context.resolve();
852
+
853
+ let name_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
854
+ assert_declaration_completion_eq!(
855
+ context,
856
+ CompletionReceiver::Expression(name_id),
857
+ [
858
+ "Foo",
859
+ "Class",
860
+ "BasicObject",
861
+ "Object",
862
+ "Kernel",
863
+ "Module",
864
+ "Baz",
865
+ "Bar",
866
+ "Foo#foo_m()",
867
+ "Baz#baz_m()",
868
+ "Bar#bar_m()"
869
+ ]
870
+ );
871
+ }
872
+
873
+ #[test]
874
+ fn completion_candidates_for_class_variables() {
875
+ let mut context = GraphTest::new();
876
+
877
+ context.index_uri(
878
+ "file:///foo.rb",
879
+ "
880
+ class Foo
881
+ @@foo_var = 1
882
+
883
+ class << self
884
+ def do_something
885
+ # Completion in this `self` context
886
+ end
887
+ end
888
+ end
889
+
890
+ class Bar < Foo
891
+ def baz
892
+ # Other completion in this `self` context
893
+ end
894
+ end
895
+ ",
896
+ );
897
+ context.resolve();
898
+
899
+ let foo_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
900
+ let name_id = Name::new(StringId::from("<Foo>"), ParentScope::Attached(foo_id), None).id();
901
+ assert_declaration_completion_eq!(
902
+ context,
903
+ CompletionReceiver::Expression(name_id),
904
+ [
905
+ "Module",
906
+ "Class",
907
+ "Object",
908
+ "BasicObject",
909
+ "Kernel",
910
+ "Foo",
911
+ "Bar",
912
+ "Foo::<Foo>#do_something()",
913
+ "Foo#@@foo_var"
914
+ ]
915
+ );
916
+
917
+ let name_id = Name::new(StringId::from("Bar"), ParentScope::None, None).id();
918
+ assert_declaration_completion_eq!(
919
+ context,
920
+ CompletionReceiver::Expression(name_id),
921
+ [
922
+ "Module",
923
+ "Class",
924
+ "Object",
925
+ "BasicObject",
926
+ "Kernel",
927
+ "Foo",
928
+ "Bar",
929
+ "Bar#baz()",
930
+ "Foo#@@foo_var"
931
+ ]
932
+ );
933
+ }
934
+
935
+ #[test]
936
+ fn completion_candidates_includes_constants_accessible_within_lexical_scope() {
937
+ let mut context = GraphTest::new();
938
+
939
+ context.index_uri(
940
+ "file:///foo.rb",
941
+ "
942
+ module Foo
943
+ CONST_A = 1
944
+
945
+ class ::Bar
946
+ def bar_m
947
+ # Completion in this `self` context
948
+ end
949
+ end
950
+ end
951
+
952
+ class Bar
953
+ def bar_m2
954
+ # Completion in this `self` context
955
+ end
956
+ end
957
+ ",
958
+ );
959
+ context.resolve();
960
+
961
+ let name_id = Name::new(
962
+ StringId::from("Bar"),
963
+ ParentScope::TopLevel,
964
+ Some(Name::new(StringId::from("Foo"), ParentScope::None, None).id()),
965
+ )
966
+ .id();
967
+ assert_declaration_completion_eq!(
968
+ context,
969
+ CompletionReceiver::Expression(name_id),
970
+ [
971
+ "Foo::CONST_A",
972
+ "Module",
973
+ "Class",
974
+ "Object",
975
+ "BasicObject",
976
+ "Kernel",
977
+ "Foo",
978
+ "Bar",
979
+ "Bar#bar_m()",
980
+ "Bar#bar_m2()"
981
+ ]
982
+ );
983
+
984
+ let name_id = Name::new(StringId::from("Bar"), ParentScope::None, None).id();
985
+ assert_declaration_completion_eq!(
986
+ context,
987
+ CompletionReceiver::Expression(name_id),
988
+ [
989
+ "Module",
990
+ "Class",
991
+ "Object",
992
+ "BasicObject",
993
+ "Kernel",
994
+ "Foo",
995
+ "Bar",
996
+ "Bar#bar_m()",
997
+ "Bar#bar_m2()"
998
+ ]
999
+ );
1000
+ }
1001
+
1002
+ #[test]
1003
+ fn completion_candidates_finds_unqualified_constant_reachable_from_namespace() {
1004
+ let mut context = GraphTest::new();
1005
+
1006
+ context.index_uri(
1007
+ "file:///foo.rb",
1008
+ "
1009
+ module Foo
1010
+ CONST = 1
1011
+
1012
+ class Bar
1013
+ def baz
1014
+ # Typing CONST here should find Foo::CONST
1015
+ end
1016
+ end
1017
+ end
1018
+ ",
1019
+ );
1020
+ context.resolve();
1021
+
1022
+ let foo_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
1023
+ let name_id = Name::new(StringId::from("Bar"), ParentScope::None, Some(foo_id)).id();
1024
+ // Foo::CONST is reachable from Foo::Bar through lexical scoping, so it must appear as a completion candidate
1025
+ // when the user types the unqualified name CONST
1026
+ assert_declaration_completion_eq!(
1027
+ context,
1028
+ CompletionReceiver::Expression(name_id),
1029
+ [
1030
+ "Foo::CONST",
1031
+ "Foo::Bar",
1032
+ "Class",
1033
+ "Object",
1034
+ "BasicObject",
1035
+ "Kernel",
1036
+ "Foo",
1037
+ "Module",
1038
+ "Foo::Bar#baz()"
1039
+ ]
1040
+ );
1041
+ }
1042
+
1043
+ #[test]
1044
+ fn completion_candidates_includes_globals() {
1045
+ let mut context = GraphTest::new();
1046
+
1047
+ context.index_uri(
1048
+ "file:///foo.rb",
1049
+ "
1050
+ $var = 1
1051
+ module Foo
1052
+ $var2 = 2
1053
+
1054
+ class Bar < BasicObject
1055
+ def bar_m
1056
+ # Completion in this `self` context
1057
+ end
1058
+ end
1059
+ end
1060
+ ",
1061
+ );
1062
+ context.resolve();
1063
+
1064
+ let name_id = Name::new(
1065
+ StringId::from("Bar"),
1066
+ ParentScope::None,
1067
+ Some(Name::new(StringId::from("Foo"), ParentScope::None, None).id()),
1068
+ )
1069
+ .id();
1070
+ assert_declaration_completion_eq!(
1071
+ context,
1072
+ CompletionReceiver::Expression(name_id),
1073
+ [
1074
+ "Foo::Bar",
1075
+ "$var2",
1076
+ "$var",
1077
+ "BasicObject",
1078
+ "Object",
1079
+ "Kernel",
1080
+ "Module",
1081
+ "Foo",
1082
+ "Class",
1083
+ "Foo::Bar#bar_m()"
1084
+ ]
1085
+ );
1086
+ }
1087
+
1088
+ #[test]
1089
+ fn namespace_access_completion_collects_constants_and_singleton_methods() {
1090
+ let mut context = GraphTest::new();
1091
+
1092
+ context.index_uri(
1093
+ "file:///foo.rb",
1094
+ "
1095
+ module Foo
1096
+ CONST = 1
1097
+ class Bar; end
1098
+
1099
+ class << self
1100
+ def class_method; end
1101
+ end
1102
+
1103
+ def instance_method; end
1104
+ end
1105
+ ",
1106
+ );
1107
+ context.resolve();
1108
+
1109
+ assert_completion_eq!(
1110
+ context,
1111
+ CompletionReceiver::NamespaceAccess(DeclarationId::from("Foo")),
1112
+ ["Foo::CONST", "Foo::Bar", "Foo::<Foo>#class_method()"]
1113
+ );
1114
+ }
1115
+
1116
+ #[test]
1117
+ fn namespace_access_completion_includes_inherited_members() {
1118
+ let mut context = GraphTest::new();
1119
+
1120
+ context.index_uri(
1121
+ "file:///foo.rb",
1122
+ "
1123
+ class Parent
1124
+ PARENT_CONST = 1
1125
+
1126
+ class << self
1127
+ def parent_class_method; end
1128
+ end
1129
+ end
1130
+
1131
+ class Child < Parent
1132
+ CHILD_CONST = 2
1133
+
1134
+ class << self
1135
+ def child_class_method; end
1136
+ end
1137
+ end
1138
+ ",
1139
+ );
1140
+ context.resolve();
1141
+
1142
+ assert_completion_eq!(
1143
+ context,
1144
+ CompletionReceiver::NamespaceAccess(DeclarationId::from("Child")),
1145
+ [
1146
+ "Child::CHILD_CONST",
1147
+ "Parent::PARENT_CONST",
1148
+ "Child::<Child>#child_class_method()",
1149
+ "Parent::<Parent>#parent_class_method()",
1150
+ ]
1151
+ );
1152
+ }
1153
+
1154
+ #[test]
1155
+ fn namespace_access_completion_deduplicates_overridden_members() {
1156
+ let mut context = GraphTest::new();
1157
+
1158
+ context.index_uri(
1159
+ "file:///foo.rb",
1160
+ "
1161
+ class Parent
1162
+ CONST = 1
1163
+
1164
+ class << self
1165
+ def shared_method; end
1166
+ end
1167
+ end
1168
+
1169
+ class Child < Parent
1170
+ CONST = 2
1171
+
1172
+ class << self
1173
+ def shared_method; end
1174
+ end
1175
+ end
1176
+ ",
1177
+ );
1178
+ context.resolve();
1179
+
1180
+ assert_completion_eq!(
1181
+ context,
1182
+ CompletionReceiver::NamespaceAccess(DeclarationId::from("Child")),
1183
+ ["Child::CONST", "Child::<Child>#shared_method()"]
1184
+ );
1185
+ }
1186
+
1187
+ #[test]
1188
+ fn namespace_access_completion_excludes_object_owned_constants() {
1189
+ let mut context = GraphTest::new();
1190
+
1191
+ context.index_uri(
1192
+ "file:///foo.rb",
1193
+ "
1194
+ class Foo
1195
+ CONST = 1
1196
+ end
1197
+
1198
+ class Bar; end
1199
+ ",
1200
+ );
1201
+ context.resolve();
1202
+
1203
+ assert_completion_eq!(
1204
+ context,
1205
+ CompletionReceiver::NamespaceAccess(DeclarationId::from("Foo")),
1206
+ ["Foo::CONST"]
1207
+ );
1208
+ }
1209
+
1210
+ #[test]
1211
+ fn namespace_access_completion_includes_constant_aliases() {
1212
+ let mut context = GraphTest::new();
1213
+
1214
+ context.index_uri(
1215
+ "file:///foo.rb",
1216
+ "
1217
+ module Foo
1218
+ Bar = String
1219
+ CONST = 1
1220
+ end
1221
+ ",
1222
+ );
1223
+ context.resolve();
1224
+
1225
+ assert_completion_eq!(
1226
+ context,
1227
+ CompletionReceiver::NamespaceAccess(DeclarationId::from("Foo")),
1228
+ ["Foo::CONST", "Foo::Bar"]
1229
+ );
1230
+ }
1231
+
1232
+ #[test]
1233
+ fn namespace_access_completion_follows_constant_alias() {
1234
+ let mut context = GraphTest::new();
1235
+
1236
+ context.index_uri(
1237
+ "file:///foo.rb",
1238
+ "
1239
+ class Original
1240
+ CONST = 1
1241
+ class Nested; end
1242
+
1243
+ class << self
1244
+ def class_method; end
1245
+ end
1246
+ end
1247
+
1248
+ module Foo
1249
+ MyOriginal = Original
1250
+ end
1251
+ ",
1252
+ );
1253
+ context.resolve();
1254
+
1255
+ assert_completion_eq!(
1256
+ context,
1257
+ CompletionReceiver::NamespaceAccess(DeclarationId::from("Foo::MyOriginal")),
1258
+ [
1259
+ "Original::CONST",
1260
+ "Original::Nested",
1261
+ "Original::<Original>#class_method()"
1262
+ ]
1263
+ );
1264
+ }
1265
+
1266
+ #[test]
1267
+ fn namespace_access_completion_follows_chained_constant_alias() {
1268
+ let mut context = GraphTest::new();
1269
+
1270
+ context.index_uri(
1271
+ "file:///foo.rb",
1272
+ "
1273
+ class Original
1274
+ CONST = 1
1275
+
1276
+ class << self
1277
+ def class_method; end
1278
+ end
1279
+ end
1280
+
1281
+ Alias1 = Original
1282
+ Alias2 = Alias1
1283
+ ",
1284
+ );
1285
+ context.resolve();
1286
+
1287
+ assert_completion_eq!(
1288
+ context,
1289
+ CompletionReceiver::NamespaceAccess(DeclarationId::from("Alias2")),
1290
+ ["Original::CONST", "Original::<Original>#class_method()"]
1291
+ );
1292
+ }
1293
+
1294
+ #[test]
1295
+ fn namespace_access_completion_on_basic_object_subclass() {
1296
+ let mut context = GraphTest::new();
1297
+
1298
+ context.index_uri(
1299
+ "file:///foo.rb",
1300
+ "
1301
+ class Foo < BasicObject
1302
+ CONST = 1
1303
+
1304
+ class << self
1305
+ def class_method; end
1306
+ end
1307
+ end
1308
+
1309
+ class Bar; end
1310
+ ",
1311
+ );
1312
+ context.resolve();
1313
+
1314
+ assert_completion_eq!(
1315
+ context,
1316
+ CompletionReceiver::NamespaceAccess(DeclarationId::from("Foo")),
1317
+ ["Foo::CONST", "Foo::<Foo>#class_method()"]
1318
+ );
1319
+ }
1320
+
1321
+ #[test]
1322
+ fn namespace_access_completion_includes_module_members() {
1323
+ let mut context = GraphTest::new();
1324
+
1325
+ context.index_uri(
1326
+ "file:///foo.rb",
1327
+ "
1328
+ module Bar
1329
+ CONST = 1
1330
+
1331
+ class << self
1332
+ def bar_class_method; end
1333
+ end
1334
+ end
1335
+
1336
+ class Foo
1337
+ FOO_CONST = 2
1338
+ include Bar
1339
+
1340
+ class << self
1341
+ def foo_class_method; end
1342
+ end
1343
+ end
1344
+ ",
1345
+ );
1346
+ context.resolve();
1347
+
1348
+ assert_completion_eq!(
1349
+ context,
1350
+ CompletionReceiver::NamespaceAccess(DeclarationId::from("Foo")),
1351
+ ["Foo::FOO_CONST", "Bar::CONST", "Foo::<Foo>#foo_class_method()"]
1352
+ );
1353
+ }
1354
+
1355
+ #[test]
1356
+ fn method_call_completion_collects_instance_methods() {
1357
+ let mut context = GraphTest::new();
1358
+
1359
+ context.index_uri(
1360
+ "file:///foo.rb",
1361
+ "
1362
+ class Foo
1363
+ CONST = 1
1364
+
1365
+ def bar; end
1366
+ def baz; end
1367
+
1368
+ class << self
1369
+ def class_method; end
1370
+ end
1371
+ end
1372
+ ",
1373
+ );
1374
+ context.resolve();
1375
+
1376
+ assert_completion_eq!(
1377
+ context,
1378
+ CompletionReceiver::MethodCall(DeclarationId::from("Foo")),
1379
+ ["Foo#baz()", "Foo#bar()"]
1380
+ );
1381
+ }
1382
+
1383
+ #[test]
1384
+ fn method_call_completion_follows_constant_alias() {
1385
+ let mut context = GraphTest::new();
1386
+
1387
+ context.index_uri(
1388
+ "file:///foo.rb",
1389
+ "
1390
+ class Original
1391
+ def bar; end
1392
+ def baz; end
1393
+
1394
+ class << self
1395
+ def class_method; end
1396
+ end
1397
+ end
1398
+
1399
+ module Foo
1400
+ MyOriginal = Original
1401
+ end
1402
+ ",
1403
+ );
1404
+ context.resolve();
1405
+
1406
+ assert_completion_eq!(
1407
+ context,
1408
+ CompletionReceiver::MethodCall(DeclarationId::from("Foo::MyOriginal")),
1409
+ ["Original#baz()", "Original#bar()"]
1410
+ );
1411
+ }
1412
+
1413
+ #[test]
1414
+ fn method_call_completion_includes_inherited_methods() {
1415
+ let mut context = GraphTest::new();
1416
+
1417
+ context.index_uri(
1418
+ "file:///foo.rb",
1419
+ "
1420
+ class Parent
1421
+ def parent_method; end
1422
+ end
1423
+
1424
+ class Child < Parent
1425
+ def child_method; end
1426
+ end
1427
+ ",
1428
+ );
1429
+ context.resolve();
1430
+
1431
+ assert_completion_eq!(
1432
+ context,
1433
+ CompletionReceiver::MethodCall(DeclarationId::from("Child")),
1434
+ ["Child#child_method()", "Parent#parent_method()"]
1435
+ );
1436
+ }
1437
+
1438
+ #[test]
1439
+ fn method_call_completion_includes_methods_from_included_modules() {
1440
+ let mut context = GraphTest::new();
1441
+
1442
+ context.index_uri(
1443
+ "file:///foo.rb",
1444
+ "
1445
+ module Mixin
1446
+ def mixin_method; end
1447
+ end
1448
+
1449
+ class Foo
1450
+ include Mixin
1451
+
1452
+ def foo_method; end
1453
+ end
1454
+ ",
1455
+ );
1456
+ context.resolve();
1457
+
1458
+ assert_completion_eq!(
1459
+ context,
1460
+ CompletionReceiver::MethodCall(DeclarationId::from("Foo")),
1461
+ ["Foo#foo_method()", "Mixin#mixin_method()"]
1462
+ );
1463
+ }
1464
+
1465
+ #[test]
1466
+ fn method_call_completion_deduplicates_overridden_methods() {
1467
+ let mut context = GraphTest::new();
1468
+
1469
+ context.index_uri(
1470
+ "file:///foo.rb",
1471
+ "
1472
+ class Parent
1473
+ def shared_method; end
1474
+ def parent_only; end
1475
+ end
1476
+
1477
+ class Child < Parent
1478
+ def shared_method; end
1479
+ def child_only; end
1480
+ end
1481
+ ",
1482
+ );
1483
+ context.resolve();
1484
+
1485
+ assert_completion_eq!(
1486
+ context,
1487
+ CompletionReceiver::MethodCall(DeclarationId::from("Child")),
1488
+ ["Child#shared_method()", "Child#child_only()", "Parent#parent_only()"]
1489
+ );
1490
+ }
1491
+
1492
+ #[test]
1493
+ fn method_call_completion_excludes_non_method_members() {
1494
+ let mut context = GraphTest::new();
1495
+
1496
+ context.index_uri(
1497
+ "file:///foo.rb",
1498
+ "
1499
+ class Foo
1500
+ CONST = 1
1501
+ @@class_var = 2
1502
+
1503
+ def initialize
1504
+ @ivar = 3
1505
+ end
1506
+
1507
+ def bar; end
1508
+ end
1509
+ ",
1510
+ );
1511
+ context.resolve();
1512
+
1513
+ assert_completion_eq!(
1514
+ context,
1515
+ CompletionReceiver::MethodCall(DeclarationId::from("Foo")),
1516
+ ["Foo#initialize()", "Foo#bar()"]
1517
+ );
1518
+ }
1519
+
1520
+ #[test]
1521
+ fn method_call_completion_at_singleton_level() {
1522
+ let mut context = GraphTest::new();
1523
+
1524
+ context.index_uri(
1525
+ "file:///foo.rb",
1526
+ "
1527
+ class Foo
1528
+ def self.bar; end
1529
+
1530
+ class << self
1531
+ def baz; end
1532
+ end
1533
+ end
1534
+ ",
1535
+ );
1536
+ context.resolve();
1537
+
1538
+ assert_completion_eq!(
1539
+ context,
1540
+ CompletionReceiver::MethodCall(DeclarationId::from("Foo::<Foo>")),
1541
+ ["Foo::<Foo>#baz()", "Foo::<Foo>#bar()"]
1542
+ );
1543
+ }
1544
+
1545
+ #[test]
1546
+ fn method_argument_completion_includes_keyword_params() {
1547
+ let mut context = GraphTest::new();
1548
+
1549
+ context.index_uri(
1550
+ "file:///foo.rb",
1551
+ "
1552
+ class Foo
1553
+ def greet(name:, greeting: 'hello'); end
1554
+ end
1555
+ ",
1556
+ );
1557
+ context.resolve();
1558
+
1559
+ let name_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
1560
+ assert_declaration_completion_eq!(
1561
+ context,
1562
+ CompletionReceiver::MethodArgument {
1563
+ self_name_id: name_id,
1564
+ method_decl_id: DeclarationId::from("Foo#greet()"),
1565
+ },
1566
+ [
1567
+ "Class",
1568
+ "Object",
1569
+ "BasicObject",
1570
+ "Kernel",
1571
+ "Foo",
1572
+ "Module",
1573
+ "Foo#greet()",
1574
+ "name:",
1575
+ "greeting:"
1576
+ ]
1577
+ );
1578
+ }
1579
+
1580
+ #[test]
1581
+ fn method_argument_completion_no_keyword_params() {
1582
+ let mut context = GraphTest::new();
1583
+
1584
+ context.index_uri(
1585
+ "file:///foo.rb",
1586
+ "
1587
+ class Foo
1588
+ def bar(x, y); end
1589
+ end
1590
+ ",
1591
+ );
1592
+ context.resolve();
1593
+
1594
+ let name_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
1595
+ assert_declaration_completion_eq!(
1596
+ context,
1597
+ CompletionReceiver::MethodArgument {
1598
+ self_name_id: name_id,
1599
+ method_decl_id: DeclarationId::from("Foo#bar()"),
1600
+ },
1601
+ ["Class", "Object", "BasicObject", "Kernel", "Foo", "Module", "Foo#bar()"]
1602
+ );
1603
+ }
1604
+
1605
+ #[test]
1606
+ fn method_argument_completion_mixed_params() {
1607
+ let mut context = GraphTest::new();
1608
+
1609
+ context.index_uri(
1610
+ "file:///foo.rb",
1611
+ "
1612
+ class Foo
1613
+ def search(query, limit:, offset: 0, **opts); end
1614
+ end
1615
+ ",
1616
+ );
1617
+ context.resolve();
1618
+
1619
+ let name_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
1620
+ assert_declaration_completion_eq!(
1621
+ context,
1622
+ CompletionReceiver::MethodArgument {
1623
+ self_name_id: name_id,
1624
+ method_decl_id: DeclarationId::from("Foo#search()"),
1625
+ },
1626
+ // Only RequiredKeyword and OptionalKeyword, not RestKeyword (**opts)
1627
+ [
1628
+ "Class",
1629
+ "Object",
1630
+ "BasicObject",
1631
+ "Kernel",
1632
+ "Foo",
1633
+ "Module",
1634
+ "Foo#search()",
1635
+ "limit:",
1636
+ "offset:"
1637
+ ]
1638
+ );
1639
+ }
1640
+
1641
+ #[test]
1642
+ fn first_entry_is_always_used_overridden_methods() {
1643
+ let mut context = GraphTest::new();
1644
+ context.index_uri(
1645
+ "file:///foo.rb",
1646
+ "
1647
+ class Foo
1648
+ def bar(first:, second:); end
1649
+ end
1650
+ ",
1651
+ );
1652
+ context.index_uri(
1653
+ "file:///foo2.rb",
1654
+ "
1655
+ class Foo
1656
+ def bar(first:); end
1657
+ end
1658
+ ",
1659
+ );
1660
+ context.resolve();
1661
+
1662
+ let name_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
1663
+ assert_declaration_completion_eq!(
1664
+ context,
1665
+ CompletionReceiver::MethodArgument {
1666
+ self_name_id: name_id,
1667
+ method_decl_id: DeclarationId::from("Foo#bar()"),
1668
+ },
1669
+ [
1670
+ "Class",
1671
+ "Object",
1672
+ "BasicObject",
1673
+ "Kernel",
1674
+ "Foo",
1675
+ "Module",
1676
+ "Foo#bar()",
1677
+ "first:",
1678
+ "second:"
1679
+ ]
1680
+ );
1681
+ }
1682
+
1683
+ #[test]
1684
+ fn expression_completion_includes_keywords() {
1685
+ let mut context = GraphTest::new();
1686
+ context.index_uri("file:///foo.rb", "class Foo; end");
1687
+ context.resolve();
1688
+
1689
+ let name_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
1690
+ assert_completion_eq!(
1691
+ context,
1692
+ CompletionReceiver::Expression(name_id),
1693
+ [
1694
+ "Class",
1695
+ "Object",
1696
+ "BasicObject",
1697
+ "Kernel",
1698
+ "Foo",
1699
+ "Module",
1700
+ "BEGIN",
1701
+ "END",
1702
+ "__ENCODING__",
1703
+ "__FILE__",
1704
+ "__LINE__",
1705
+ "alias",
1706
+ "and",
1707
+ "begin",
1708
+ "break",
1709
+ "case",
1710
+ "class",
1711
+ "def",
1712
+ "defined?",
1713
+ "do",
1714
+ "else",
1715
+ "elsif",
1716
+ "end",
1717
+ "ensure",
1718
+ "false",
1719
+ "for",
1720
+ "if",
1721
+ "in",
1722
+ "module",
1723
+ "next",
1724
+ "nil",
1725
+ "not",
1726
+ "or",
1727
+ "redo",
1728
+ "rescue",
1729
+ "retry",
1730
+ "return",
1731
+ "self",
1732
+ "super",
1733
+ "then",
1734
+ "true",
1735
+ "undef",
1736
+ "unless",
1737
+ "until",
1738
+ "when",
1739
+ "while",
1740
+ "yield",
1741
+ ]
1742
+ );
1743
+ }
1744
+
1745
+ #[test]
1746
+ fn method_argument_completion_includes_keywords() {
1747
+ let mut context = GraphTest::new();
1748
+ context.index_uri("file:///foo.rb", "class Foo; def bar(name:); end; end");
1749
+ context.resolve();
1750
+
1751
+ let name_id = Name::new(StringId::from("Foo"), ParentScope::None, None).id();
1752
+ assert_completion_eq!(
1753
+ context,
1754
+ CompletionReceiver::MethodArgument {
1755
+ self_name_id: name_id,
1756
+ method_decl_id: DeclarationId::from("Foo#bar()"),
1757
+ },
1758
+ [
1759
+ "Class",
1760
+ "Object",
1761
+ "BasicObject",
1762
+ "Kernel",
1763
+ "Foo",
1764
+ "Module",
1765
+ "Foo#bar()",
1766
+ "BEGIN",
1767
+ "END",
1768
+ "__ENCODING__",
1769
+ "__FILE__",
1770
+ "__LINE__",
1771
+ "alias",
1772
+ "and",
1773
+ "begin",
1774
+ "break",
1775
+ "case",
1776
+ "class",
1777
+ "def",
1778
+ "defined?",
1779
+ "do",
1780
+ "else",
1781
+ "elsif",
1782
+ "end",
1783
+ "ensure",
1784
+ "false",
1785
+ "for",
1786
+ "if",
1787
+ "in",
1788
+ "module",
1789
+ "next",
1790
+ "nil",
1791
+ "not",
1792
+ "or",
1793
+ "redo",
1794
+ "rescue",
1795
+ "retry",
1796
+ "return",
1797
+ "self",
1798
+ "super",
1799
+ "then",
1800
+ "true",
1801
+ "undef",
1802
+ "unless",
1803
+ "until",
1804
+ "when",
1805
+ "while",
1806
+ "yield",
1807
+ "name:",
1808
+ ]
1809
+ );
1810
+ }
1811
+
1812
+ #[test]
1813
+ fn namespace_access_completion_excludes_keywords() {
1814
+ let mut context = GraphTest::new();
1815
+ context.index_uri("file:///foo.rb", "class Foo; CONST = 1; end");
1816
+ context.resolve();
1817
+
1818
+ let candidates = completion_candidates(
1819
+ context.graph(),
1820
+ CompletionContext::new(CompletionReceiver::NamespaceAccess(DeclarationId::from("Foo"))),
1821
+ )
1822
+ .unwrap();
1823
+
1824
+ assert!(!candidates.iter().any(|c| matches!(c, CompletionCandidate::Keyword(_))));
1825
+ }
1826
+
1827
+ #[test]
1828
+ fn method_call_completion_excludes_keywords() {
1829
+ let mut context = GraphTest::new();
1830
+ context.index_uri("file:///foo.rb", "class Foo; def bar; end; end");
1831
+ context.resolve();
1832
+
1833
+ let candidates = completion_candidates(
1834
+ context.graph(),
1835
+ CompletionContext::new(CompletionReceiver::MethodCall(DeclarationId::from("Foo"))),
1836
+ )
1837
+ .unwrap();
1838
+
1839
+ assert!(!candidates.iter().any(|c| matches!(c, CompletionCandidate::Keyword(_))));
1840
+ }
1841
+ }