rubydex 0.1.0.beta12-aarch64-linux

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 (109) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +23 -0
  3. data/README.md +125 -0
  4. data/THIRD_PARTY_LICENSES.html +4562 -0
  5. data/exe/rdx +47 -0
  6. data/ext/rubydex/declaration.c +453 -0
  7. data/ext/rubydex/declaration.h +23 -0
  8. data/ext/rubydex/definition.c +284 -0
  9. data/ext/rubydex/definition.h +28 -0
  10. data/ext/rubydex/diagnostic.c +6 -0
  11. data/ext/rubydex/diagnostic.h +11 -0
  12. data/ext/rubydex/document.c +97 -0
  13. data/ext/rubydex/document.h +10 -0
  14. data/ext/rubydex/extconf.rb +138 -0
  15. data/ext/rubydex/graph.c +681 -0
  16. data/ext/rubydex/graph.h +10 -0
  17. data/ext/rubydex/handle.h +44 -0
  18. data/ext/rubydex/location.c +22 -0
  19. data/ext/rubydex/location.h +15 -0
  20. data/ext/rubydex/reference.c +123 -0
  21. data/ext/rubydex/reference.h +15 -0
  22. data/ext/rubydex/rubydex.c +22 -0
  23. data/ext/rubydex/utils.c +108 -0
  24. data/ext/rubydex/utils.h +34 -0
  25. data/lib/rubydex/3.2/rubydex.so +0 -0
  26. data/lib/rubydex/3.3/rubydex.so +0 -0
  27. data/lib/rubydex/3.4/rubydex.so +0 -0
  28. data/lib/rubydex/4.0/rubydex.so +0 -0
  29. data/lib/rubydex/comment.rb +17 -0
  30. data/lib/rubydex/diagnostic.rb +21 -0
  31. data/lib/rubydex/failures.rb +15 -0
  32. data/lib/rubydex/graph.rb +98 -0
  33. data/lib/rubydex/keyword.rb +17 -0
  34. data/lib/rubydex/keyword_parameter.rb +13 -0
  35. data/lib/rubydex/librubydex_sys.so +0 -0
  36. data/lib/rubydex/location.rb +90 -0
  37. data/lib/rubydex/mixin.rb +22 -0
  38. data/lib/rubydex/version.rb +5 -0
  39. data/lib/rubydex.rb +23 -0
  40. data/rbi/rubydex.rbi +422 -0
  41. data/rust/Cargo.lock +1851 -0
  42. data/rust/Cargo.toml +29 -0
  43. data/rust/about.hbs +78 -0
  44. data/rust/about.toml +10 -0
  45. data/rust/rubydex/Cargo.toml +42 -0
  46. data/rust/rubydex/src/compile_assertions.rs +13 -0
  47. data/rust/rubydex/src/diagnostic.rs +110 -0
  48. data/rust/rubydex/src/errors.rs +28 -0
  49. data/rust/rubydex/src/indexing/local_graph.rs +224 -0
  50. data/rust/rubydex/src/indexing/rbs_indexer.rs +1551 -0
  51. data/rust/rubydex/src/indexing/ruby_indexer.rs +2329 -0
  52. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +4962 -0
  53. data/rust/rubydex/src/indexing.rs +210 -0
  54. data/rust/rubydex/src/integrity.rs +279 -0
  55. data/rust/rubydex/src/job_queue.rs +205 -0
  56. data/rust/rubydex/src/lib.rs +17 -0
  57. data/rust/rubydex/src/listing.rs +371 -0
  58. data/rust/rubydex/src/main.rs +160 -0
  59. data/rust/rubydex/src/model/built_in.rs +83 -0
  60. data/rust/rubydex/src/model/comment.rs +24 -0
  61. data/rust/rubydex/src/model/declaration.rs +671 -0
  62. data/rust/rubydex/src/model/definitions.rs +1682 -0
  63. data/rust/rubydex/src/model/document.rs +222 -0
  64. data/rust/rubydex/src/model/encoding.rs +22 -0
  65. data/rust/rubydex/src/model/graph.rs +3754 -0
  66. data/rust/rubydex/src/model/id.rs +110 -0
  67. data/rust/rubydex/src/model/identity_maps.rs +58 -0
  68. data/rust/rubydex/src/model/ids.rs +60 -0
  69. data/rust/rubydex/src/model/keywords.rs +256 -0
  70. data/rust/rubydex/src/model/name.rs +298 -0
  71. data/rust/rubydex/src/model/references.rs +111 -0
  72. data/rust/rubydex/src/model/string_ref.rs +50 -0
  73. data/rust/rubydex/src/model/visibility.rs +41 -0
  74. data/rust/rubydex/src/model.rs +15 -0
  75. data/rust/rubydex/src/offset.rs +147 -0
  76. data/rust/rubydex/src/position.rs +6 -0
  77. data/rust/rubydex/src/query.rs +1841 -0
  78. data/rust/rubydex/src/resolution.rs +6517 -0
  79. data/rust/rubydex/src/stats/memory.rs +71 -0
  80. data/rust/rubydex/src/stats/orphan_report.rs +264 -0
  81. data/rust/rubydex/src/stats/timer.rs +127 -0
  82. data/rust/rubydex/src/stats.rs +11 -0
  83. data/rust/rubydex/src/test_utils/context.rs +226 -0
  84. data/rust/rubydex/src/test_utils/graph_test.rs +730 -0
  85. data/rust/rubydex/src/test_utils/local_graph_test.rs +602 -0
  86. data/rust/rubydex/src/test_utils.rs +52 -0
  87. data/rust/rubydex/src/visualization/dot.rs +192 -0
  88. data/rust/rubydex/src/visualization.rs +6 -0
  89. data/rust/rubydex/tests/cli.rs +185 -0
  90. data/rust/rubydex-mcp/Cargo.toml +28 -0
  91. data/rust/rubydex-mcp/src/main.rs +48 -0
  92. data/rust/rubydex-mcp/src/server.rs +1145 -0
  93. data/rust/rubydex-mcp/src/tools.rs +49 -0
  94. data/rust/rubydex-mcp/tests/mcp.rs +302 -0
  95. data/rust/rubydex-sys/Cargo.toml +20 -0
  96. data/rust/rubydex-sys/build.rs +14 -0
  97. data/rust/rubydex-sys/cbindgen.toml +12 -0
  98. data/rust/rubydex-sys/src/declaration_api.rs +485 -0
  99. data/rust/rubydex-sys/src/definition_api.rs +443 -0
  100. data/rust/rubydex-sys/src/diagnostic_api.rs +99 -0
  101. data/rust/rubydex-sys/src/document_api.rs +85 -0
  102. data/rust/rubydex-sys/src/graph_api.rs +948 -0
  103. data/rust/rubydex-sys/src/lib.rs +79 -0
  104. data/rust/rubydex-sys/src/location_api.rs +79 -0
  105. data/rust/rubydex-sys/src/name_api.rs +135 -0
  106. data/rust/rubydex-sys/src/reference_api.rs +267 -0
  107. data/rust/rubydex-sys/src/utils.rs +70 -0
  108. data/rust/rustfmt.toml +2 -0
  109. metadata +159 -0
@@ -0,0 +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::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(std::num::NonZero::get).unwrap_or(4);
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(std::num::NonZero::get).unwrap_or(4);
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
+ }