Skip to content

Commit d3a463f

Browse files
authored
Add keywords to completion candidates (#2317)
* Bump Sorbet version so T.let on array constants is not required * Add keywords to completion candidates When typing `foo d`, VS Code would send a completion request for `d`. And then when `foo do` is typed, it would do fuzzy search on the previous completion candidates. So currently, the completion candidates for `foo do` would be `define_singleton_method` as demonstrated in #2304. Sorbet LSP solved this issue by adding keywords to the completion candidates. And this PR does the same.
1 parent ac0ca53 commit d3a463f

4 files changed

Lines changed: 102 additions & 69 deletions

File tree

Gemfile.lock

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,14 @@ GEM
9595
simplecov_json_formatter (~> 0.1)
9696
simplecov-html (0.12.3)
9797
simplecov_json_formatter (0.1.4)
98-
sorbet (0.5.11435)
99-
sorbet-static (= 0.5.11435)
100-
sorbet-runtime (0.5.11435)
101-
sorbet-static (0.5.11435-universal-darwin)
102-
sorbet-static (0.5.11435-x86_64-linux)
103-
sorbet-static-and-runtime (0.5.11435)
104-
sorbet (= 0.5.11435)
105-
sorbet-runtime (= 0.5.11435)
98+
sorbet (0.5.11481)
99+
sorbet-static (= 0.5.11481)
100+
sorbet-runtime (0.5.11481)
101+
sorbet-static (0.5.11481-universal-darwin)
102+
sorbet-static (0.5.11481-x86_64-linux)
103+
sorbet-static-and-runtime (0.5.11481)
104+
sorbet (= 0.5.11481)
105+
sorbet-runtime (= 0.5.11481)
106106
spoom (1.3.0)
107107
erubi (>= 1.10.0)
108108
prism (>= 0.19.0)

lib/ruby_lsp/listeners/completion.rb

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,50 @@ class Completion
77
extend T::Sig
88
include Requests::Support::Common
99

10+
KEYWORDS = [
11+
"alias",
12+
"and",
13+
"begin",
14+
"BEGIN",
15+
"break",
16+
"case",
17+
"class",
18+
"def",
19+
"defined?",
20+
"do",
21+
"else",
22+
"elsif",
23+
"end",
24+
"END",
25+
"ensure",
26+
"false",
27+
"for",
28+
"if",
29+
"in",
30+
"module",
31+
"next",
32+
"nil",
33+
"not",
34+
"or",
35+
"redo",
36+
"rescue",
37+
"retry",
38+
"return",
39+
"self",
40+
"super",
41+
"then",
42+
"true",
43+
"undef",
44+
"unless",
45+
"until",
46+
"when",
47+
"while",
48+
"yield",
49+
"__ENCODING__",
50+
"__FILE__",
51+
"__LINE__",
52+
].freeze
53+
1054
sig do
1155
params(
1256
response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::CompletionItem],
@@ -277,7 +321,11 @@ def complete_require_relative(node)
277321

278322
sig { params(node: Prism::CallNode, name: String).void }
279323
def complete_methods(node, name)
280-
add_local_completions(node, name)
324+
# If the node has a receiver, then we don't need to provide local nor keyword completions
325+
if !@global_state.has_type_checker && !node.receiver
326+
add_local_completions(node, name)
327+
add_keyword_completions(node, name)
328+
end
281329

282330
type = @type_inferrer.infer_receiver_type(@node_context)
283331
return unless type
@@ -326,11 +374,6 @@ def complete_methods(node, name)
326374

327375
sig { params(node: Prism::CallNode, name: String).void }
328376
def add_local_completions(node, name)
329-
return if @global_state.has_type_checker
330-
331-
# If the call node has a receiver, then it cannot possibly be a local variable
332-
return if node.receiver
333-
334377
range = range_from_location(T.must(node.message_loc))
335378

336379
@node_context.locals_for_scope.each do |local|
@@ -349,6 +392,24 @@ def add_local_completions(node, name)
349392
end
350393
end
351394

395+
sig { params(node: Prism::CallNode, name: String).void }
396+
def add_keyword_completions(node, name)
397+
range = range_from_location(T.must(node.message_loc))
398+
399+
KEYWORDS.each do |keyword|
400+
next unless keyword.start_with?(name)
401+
402+
@response_builder << Interface::CompletionItem.new(
403+
label: keyword,
404+
text_edit: Interface::TextEdit.new(range: range, new_text: keyword),
405+
kind: Constant::CompletionItemKind::KEYWORD,
406+
data: {
407+
skip_resolve: true,
408+
},
409+
)
410+
end
411+
end
412+
352413
sig { params(label: String, node: Prism::StringNode).returns(Interface::CompletionItem) }
353414
def build_completion(label, node)
354415
# We should use the content location as we only replace the content and not the delimiters of the string

test/requests/completion_test.rb

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,27 @@
44
require "test_helper"
55

66
class CompletionTest < Minitest::Test
7+
def test_completion_keyword
8+
source = <<~RUBY
9+
foo d
10+
RUBY
11+
12+
with_server(source, stub_no_typechecker: true) do |server, uri|
13+
with_file_structure(server) do
14+
server.process_message(id: 1, method: "textDocument/completion", params: {
15+
textDocument: { uri: uri },
16+
position: { line: 0, character: 5 },
17+
})
18+
result = server.pop_response.response
19+
20+
assert_equal(3, result.count)
21+
assert_equal("def", result[0].label)
22+
assert_equal("defined?", result[1].label)
23+
assert_equal("do", result[2].label)
24+
end
25+
end
26+
end
27+
728
def test_completion_command
829
prefix = "foo/"
930
source = <<~RUBY
@@ -1008,7 +1029,7 @@ def bar
10081029
})
10091030

10101031
result = server.pop_response.response
1011-
assert_equal(["baz", "bar"], result.map(&:label))
1032+
assert_equal(["begin", "break", "baz", "bar"], result.map(&:label))
10121033
end
10131034
end
10141035

@@ -1077,7 +1098,7 @@ def do_something
10771098
position: { line: 8, character: 5 },
10781099
})
10791100
result = server.pop_response.response
1080-
assert_equal(["bar", "baz"], result.map(&:label))
1101+
assert_equal(["begin", "break", "bar", "baz"], result.map(&:label))
10811102
end
10821103
end
10831104

@@ -1108,31 +1129,31 @@ def do_something(abc1, abc2, abc3)
11081129
})
11091130

11101131
result = server.pop_response.response
1111-
assert_equal(["abc1", "abc2", "abc3"], result.map(&:label))
1132+
assert_equal(["abc1", "abc2", "abc3", "alias", "and"], result.map(&:label))
11121133

11131134
server.process_message(id: 1, method: "textDocument/completion", params: {
11141135
textDocument: { uri: uri },
11151136
position: { line: 7, character: 7 },
11161137
})
11171138

11181139
result = server.pop_response.response
1119-
assert_equal(["abc1", "abc2", "abc3", "abc4", "abc5"], result.map(&:label))
1140+
assert_equal(["abc1", "abc2", "abc3", "abc4", "abc5", "alias", "and"], result.map(&:label))
11201141

11211142
server.process_message(id: 1, method: "textDocument/completion", params: {
11221143
textDocument: { uri: uri },
11231144
position: { line: 11, character: 3 },
11241145
})
11251146

11261147
result = server.pop_response.response
1127-
assert_equal(["abc0"], result.map(&:label))
1148+
assert_equal(["abc0", "alias", "and"], result.map(&:label))
11281149

11291150
server.process_message(id: 1, method: "textDocument/completion", params: {
11301151
textDocument: { uri: uri },
11311152
position: { line: 15, character: 1 },
11321153
})
11331154

11341155
result = server.pop_response.response
1135-
assert_equal(["abc"], result.map(&:label))
1156+
assert_equal(["abc", "alias", "and"], result.map(&:label))
11361157
end
11371158
end
11381159

vscode/snippets.json

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,6 @@
6262
"body": ["module $1", " def $2", " $0", " end", "end", ""],
6363
"description": "New Ruby module."
6464
},
65-
"Begin block": {
66-
"prefix": ["begin"],
67-
"body": ["begin", " $0", "end", ""],
68-
"description": "New Ruby begin block."
69-
},
7065
"Begin rescue block": {
7166
"prefix": ["begin"],
7267
"body": ["begin", " $0", "rescue $1", "end", ""],
@@ -77,21 +72,6 @@
7772
"body": ["begin", " $0", "rescue $1", "ensure", "end", ""],
7873
"description": "New Ruby begin block with rescue and ensure."
7974
},
80-
"While": {
81-
"prefix": ["while"],
82-
"body": ["while $1", " $0", "end", ""],
83-
"description": "New Ruby while loop."
84-
},
85-
"Until": {
86-
"prefix": ["until"],
87-
"body": ["until $1", " $0", "end", ""],
88-
"description": "New Ruby until loop."
89-
},
90-
"For": {
91-
"prefix": ["for"],
92-
"body": ["for $1 in $2", " $0", "end", ""],
93-
"description": "New Ruby for loop."
94-
},
9575
"Each inline": {
9676
"prefix": ["each"],
9777
"body": ["each { |$1| $0 }"],
@@ -142,11 +122,6 @@
142122
"body": ["find do |$1|", " $0", "end"],
143123
"description": "New Ruby find loop."
144124
},
145-
"Define method": {
146-
"prefix": ["def"],
147-
"body": ["def $1", " $0", "end"],
148-
"description": "New method."
149-
},
150125
"Define singleton method": {
151126
"prefix": ["def"],
152127
"body": ["def self.$1", " $0", "end"],
@@ -167,11 +142,6 @@
167142
"body": ["attr_writer :$1"],
168143
"description": "New attribute writer."
169144
},
170-
"If": {
171-
"prefix": ["if"],
172-
"body": ["if $1", " $0", "end"],
173-
"description": "New if statement."
174-
},
175145
"If else": {
176146
"prefix": ["if"],
177147
"body": ["if $1", " $0", "else", "end"],
@@ -181,24 +151,5 @@
181151
"prefix": ["if"],
182152
"body": ["if $1", " $0", "elsif $2", " $0", "end"],
183153
"description": "New if statement with elsif."
184-
},
185-
"Unless": {
186-
"prefix": ["unless"],
187-
"body": ["unless $1", " $0", "end"],
188-
"description": "New unless statement."
189-
},
190-
"Case": {
191-
"prefix": ["case"],
192-
"body": [
193-
"case $0",
194-
"when $1",
195-
" $2",
196-
"when $3",
197-
" $4",
198-
"else",
199-
" $5",
200-
"end"
201-
],
202-
"description": "New case statement."
203154
}
204155
}

0 commit comments

Comments
 (0)