Skip to content

Commit 0713a1d

Browse files
committed
Fix completion at beginning and in partial words
Fixes completion at beginning of file by removing a check on abs_position and by excluding 'QualifiedName' from the results of a completion. Fixes completion in partial words by replacing remove_char with remove_word. Leaves remove_char in as a final option for '$facts[' completion
1 parent 715db55 commit 0713a1d

File tree

3 files changed

+94
-15
lines changed

3 files changed

+94
-15
lines changed

server/lib/puppet-languageserver/completion_provider.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ def self.complete(content, line_num, char_num)
44
items = []
55
incomplete = false
66

7-
item = PuppetLanguageServer::PuppetParserHelper.object_under_cursor(content, line_num, char_num, true)
7+
item = PuppetLanguageServer::PuppetParserHelper.object_under_cursor(content, line_num, char_num, true, [Puppet::Pops::Model::QualifiedName])
88

99
if item.nil?
1010
# We are in the root of the document.

server/lib/puppet-languageserver/puppet_parser_helper.rb

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,28 @@
11
module PuppetLanguageServer
22
module PuppetParserHelper
3-
def self.remove_char_at(content, line_offsets, line_num, char_num)
3+
def self.remove_chars_starting_at(content, line_offsets, line_num, char_num, num_chars_to_remove)
44
line_offset = line_offsets[line_num]
55
raise if line_offset.nil?
66

77
# Remove the offending character
8-
new_content = content.slice(0, line_offset + char_num - 1) + content.slice(line_offset + char_num, content.length - 1)
8+
new_content = content.slice(0, line_offset + char_num - num_chars_to_remove) + content.slice(line_offset + char_num, content.length - num_chars_to_remove)
99

1010
new_content
1111
end
1212

13+
def self.remove_char_at(content, line_offsets, line_num, char_num)
14+
remove_chars_starting_at(content, line_offsets, line_num, char_num, 1)
15+
end
16+
17+
def self.get_char_at(content, line_offsets, line_num, char_num)
18+
line_offset = line_offsets[line_num]
19+
raise if line_offset.nil?
20+
21+
absolute_offset = line_offset + (char_num - 1)
22+
23+
content[absolute_offset]
24+
end
25+
1326
def self.insert_text_at(content, line_offsets, line_num, char_num, text)
1427
# Insert text after where the cursor is
1528
# This helps due to syntax errors like `$facts[]` or `ensure =>`
@@ -43,7 +56,7 @@ def self.get_line_at(content, line_offsets, line_num)
4356
end
4457
end
4558

46-
def self.object_under_cursor(content, line_num, char_num, multiple_attempts = false)
59+
def self.object_under_cursor(content, line_num, char_num, multiple_attempts = false, disallowed_classes = [])
4760
# Use Puppet to generate the AST
4861
parser = Puppet::Pops::Parser::Parser.new
4962

@@ -53,14 +66,25 @@ def self.object_under_cursor(content, line_num, char_num, multiple_attempts = fa
5366

5467
result = nil
5568
move_offset = 0
56-
%i[noop remove_char try_quotes try_quotes_and_comma].each do |method|
69+
%i[noop remove_word try_quotes try_quotes_and_comma remove_char].each do |method|
5770
new_content = nil
5871
case method
5972
when :noop
6073
new_content = content
6174
when :remove_char
6275
new_content = remove_char_at(content, line_offsets, line_num, char_num)
6376
move_offset = -1
77+
when :remove_word
78+
next_char = get_char_at(content, line_offsets, line_num, char_num)
79+
80+
while /[[:word:]]/.match(next_char)
81+
move_offset -= 1
82+
next_char = get_char_at(content, line_offsets, line_num, char_num + move_offset)
83+
84+
break if char_num + move_offset < 0
85+
end
86+
87+
new_content = remove_chars_starting_at(content, line_offsets, line_num, char_num, -move_offset)
6488
when :try_quotes
6589
# Perhaps try inserting double quotes. Useful in empty arrays or during variable assignment
6690
# Grab the line up to the cursor character + 1
@@ -98,7 +122,7 @@ def self.object_under_cursor(content, line_num, char_num, multiple_attempts = fa
98122
# If during paring we modified the source we may need to change the cursor location
99123
abs_offset = result.line_offsets[line_num] + char_num + move_offset
100124
# Typically we're completing after something was typed, so go back one char
101-
abs_offset -= 1 if abs_offset > 0
125+
abs_offset -= 1
102126

103127
# Enumerate the AST looking for items that span the line/char we want.
104128
# Once we have all valid items, sort them by the smallest span. Typically the smallest span
@@ -107,7 +131,7 @@ def self.object_under_cursor(content, line_num, char_num, multiple_attempts = fa
107131
# TODO: Should probably walk the AST and only look for the deepest child, but integer sorting
108132
# is so much easier and faster.
109133
valid_models = result.model.eAllContents.select do |item|
110-
!item.offset.nil? && !item.length.nil? && abs_offset >= item.offset && abs_offset <= item.offset + item.length
134+
!item.offset.nil? && !item.length.nil? && abs_offset >= item.offset && abs_offset <= item.offset + item.length && !disallowed_classes.include?(item.class)
111135
end
112136
valid_models.sort! { |a, b| a.length - b.length }
113137

server/spec/integration/puppet-languageserver/completion_provider_spec.rb

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,20 @@ class Alice {
3636
}
3737

3838
describe "When inside the root of the manifest" do
39-
let(:line_num) { 8 }
4039
let(:char_num) { 0 }
4140
let(:expected_types) { ['keyword','resource_type','function'] }
4241

43-
it 'should return a list of keyword, resource_type, function' do
44-
result = subject.complete(content, line_num, char_num)
42+
[0, 8].each do |line_num|
43+
it "should return a list of keyword, resource_type, function regardless of cursor location (Testing line #{line_num})" do
44+
result = subject.complete(content, line_num, char_num)
4545

46-
result['items'].each do |item|
47-
expect(item).to be_completion_item_with_type(expected_types)
48-
end
46+
result['items'].each do |item|
47+
expect(item).to be_completion_item_with_type(expected_types)
48+
end
4949

50-
expected_types.each do |typename|
51-
expect(number_of_completion_item_with_type(result,typename)).to be > 0
50+
expected_types.each do |typename|
51+
expect(number_of_completion_item_with_type(result,typename)).to be > 0
52+
end
5253
end
5354
end
5455
end
@@ -90,6 +91,60 @@ class Alice {
9091
end
9192
end
9293

94+
context "Given a simple manifest mid-typing" do
95+
let(:content_empty) { <<-EOT
96+
c
97+
EOT
98+
}
99+
100+
let(:content_simple) { <<-EOT
101+
user { 'Charlie':
102+
103+
ensure => 'present',
104+
name => 'name',
105+
}
106+
107+
r
108+
EOT
109+
}
110+
111+
describe "When typing inside the root of an empty manifest" do
112+
let(:line_num) { 0 }
113+
let(:char_num) { 1 }
114+
let(:expected_types) { ['keyword','resource_type','function'] }
115+
116+
it "should return a list of keyword, resource_type, function" do
117+
result = subject.complete(content_empty, line_num, char_num)
118+
119+
result['items'].each do |item|
120+
expect(item).to be_completion_item_with_type(expected_types)
121+
end
122+
123+
expected_types.each do |typename|
124+
expect(number_of_completion_item_with_type(result,typename)).to be > 0
125+
end
126+
end
127+
end
128+
129+
describe "When typing inside the root of a non-empty manifest" do
130+
let(:line_num) { 6 }
131+
let(:char_num) { 1 }
132+
let(:expected_types) { ['keyword','resource_type','function'] }
133+
134+
it "should return a list of keyword, resource_type, function" do
135+
result = subject.complete(content_simple, line_num, char_num)
136+
137+
result['items'].each do |item|
138+
expect(item).to be_completion_item_with_type(expected_types)
139+
end
140+
141+
expected_types.each do |typename|
142+
expect(number_of_completion_item_with_type(result,typename)).to be > 0
143+
end
144+
end
145+
end
146+
end
147+
93148
context '$facts variable' do
94149
describe "With newlines at the beginning of the document and inside the brackets of $facts" do
95150
let(:content) { <<-EOT

0 commit comments

Comments
 (0)