Skip to content

Commit f1e1c10

Browse files
committed
Tests for the language server
1 parent 828f7cb commit f1e1c10

File tree

4 files changed

+233
-13
lines changed

4 files changed

+233
-13
lines changed

lib/syntax_tree/language_server.rb

+1-3
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,7 @@ def run
7070
id:,
7171
params: { textDocument: { uri: } }
7272
}
73-
output = []
74-
PP.pp(SyntaxTree.parse(store[uri]), output)
75-
write(id: id, result: output.join)
73+
write(id: id, result: PP.pp(SyntaxTree.parse(store[uri]), +""))
7674
in method: %r{\$/.+}
7775
# ignored
7876
else

lib/syntax_tree/language_server/inlay_hints.rb

+9
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ def visit(node)
3838
#
3939
def visit_assign(node)
4040
parentheses(node.location) if stack[-2].is_a?(Params)
41+
super
4142
end
4243

4344
# Adds parentheses around binary expressions to make it clear which
@@ -57,6 +58,8 @@ def visit_binary(node)
5758
parentheses(node.location)
5859
else
5960
end
61+
62+
super
6063
end
6164

6265
# Adds parentheses around ternary operators contained within certain
@@ -73,6 +76,8 @@ def visit_if_op(node)
7376
if stack[-2] in Assign | Binary | IfOp | OpAssign
7477
parentheses(node.location)
7578
end
79+
80+
super
7681
end
7782

7883
# Adds the implicitly rescued StandardError into a bare rescue clause. For
@@ -92,6 +97,8 @@ def visit_rescue(node)
9297
if node.exception.nil?
9398
after[node.location.start_char + "rescue".length] << " StandardError"
9499
end
100+
101+
super
95102
end
96103

97104
# Adds parentheses around unary statements using the - operator that are
@@ -107,6 +114,8 @@ def visit_unary(node)
107114
if stack[-2].is_a?(Binary) && (node.operator == "-")
108115
parentheses(node.location)
109116
end
117+
118+
super
110119
end
111120

112121
def self.find(program)

lib/syntax_tree/node.rb

+22-10
Original file line numberDiff line numberDiff line change
@@ -2129,11 +2129,13 @@ def format(q)
21292129
#
21302130
# break
21312131
#
2132-
in [Paren[
2133-
contents: {
2134-
body: [ArrayLiteral[contents: { parts: [_, _, *] }] => array]
2135-
}
2136-
]]
2132+
in [
2133+
Paren[
2134+
contents: {
2135+
body: [ArrayLiteral[contents: { parts: [_, _, *] }] => array]
2136+
}
2137+
]
2138+
]
21372139
# Here we have a single argument that is a set of parentheses wrapping
21382140
# an array literal that has at least 2 elements. We're going to print
21392141
# the contents of the array directly. This would be like if we had:
@@ -2146,7 +2148,9 @@ def format(q)
21462148
#
21472149
q.text(" ")
21482150
format_array_contents(q, array)
2149-
in [Paren[contents: { body: [ArrayLiteral => statement] }]]
2151+
in [
2152+
Paren[contents: { body: [ArrayLiteral => statement] }]
2153+
]
21502154
# Here we have a single argument that is a set of parentheses wrapping
21512155
# an array literal that has 0 or 1 elements. We're going to skip the
21522156
# parentheses but print the array itself. This would be like if we
@@ -2174,15 +2178,19 @@ def format(q)
21742178
#
21752179
q.text(" ")
21762180
q.format(statement)
2177-
in [Paren => part]
2181+
in [
2182+
Paren => part
2183+
]
21782184
# Here we have a single argument that is a set of parentheses. We're
21792185
# going to print the parentheses themselves as if they were the set of
21802186
# arguments. This would be like if we had:
21812187
#
21822188
# break(foo.bar)
21832189
#
21842190
q.format(part)
2185-
in [ArrayLiteral[contents: { parts: [_, _, *] }] => array]
2191+
in [
2192+
ArrayLiteral[contents: { parts: [_, _, *] }] => array
2193+
]
21862194
# Here there is a single argument that is an array literal with at
21872195
# least two elements. We skip directly into the array literal's
21882196
# elements in order to print the contents. This would be like if we
@@ -2196,7 +2204,9 @@ def format(q)
21962204
#
21972205
q.text(" ")
21982206
format_array_contents(q, array)
2199-
in [ArrayLiteral => part]
2207+
in [
2208+
ArrayLiteral => part
2209+
]
22002210
# Here there is a single argument that is an array literal with 0 or 1
22012211
# elements. In this case we're going to print the array as it is
22022212
# because skipping the brackets would change the remaining. This would
@@ -2207,7 +2217,9 @@ def format(q)
22072217
#
22082218
q.text(" ")
22092219
q.format(part)
2210-
in [_]
2220+
in [
2221+
_
2222+
]
22112223
# Here there is a single argument that hasn't matched one of our
22122224
# previous cases. We're going to print the argument as it is. This
22132225
# would be like if we had:

test/language_server_test.rb

+201
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "test_helper"
4+
require "syntax_tree/language_server"
5+
6+
module SyntaxTree
7+
class LanguageServerTest < Minitest::Test
8+
class Initialize < Struct.new(:id)
9+
def to_hash
10+
{ method: "initialize", id: id }
11+
end
12+
end
13+
14+
class Shutdown
15+
def to_hash
16+
{ method: "shutdown" }
17+
end
18+
end
19+
20+
class TextDocumentDidOpen < Struct.new(:uri, :text)
21+
def to_hash
22+
{
23+
method: "textDocument/didOpen",
24+
params: {
25+
textDocument: {
26+
uri: uri,
27+
text: text
28+
}
29+
}
30+
}
31+
end
32+
end
33+
34+
class TextDocumentDidChange < Struct.new(:uri, :text)
35+
def to_hash
36+
{
37+
method: "textDocument/didChange",
38+
params: {
39+
textDocument: {
40+
uri: uri
41+
},
42+
contentChanges: [{ text: text }]
43+
}
44+
}
45+
end
46+
end
47+
48+
class TextDocumentDidClose < Struct.new(:uri)
49+
def to_hash
50+
{
51+
method: "textDocument/didClose",
52+
params: {
53+
textDocument: {
54+
uri: uri
55+
}
56+
}
57+
}
58+
end
59+
end
60+
61+
class TextDocumentFormatting < Struct.new(:id, :uri)
62+
def to_hash
63+
{
64+
method: "textDocument/formatting",
65+
id: id,
66+
params: {
67+
textDocument: {
68+
uri: uri
69+
}
70+
}
71+
}
72+
end
73+
end
74+
75+
class TextDocumentInlayHints < Struct.new(:id, :uri)
76+
def to_hash
77+
{
78+
method: "textDocument/inlayHints",
79+
id: id,
80+
params: {
81+
textDocument: {
82+
uri: uri
83+
}
84+
}
85+
}
86+
end
87+
end
88+
89+
class SyntaxTreeVisualizing < Struct.new(:id, :uri)
90+
def to_hash
91+
{
92+
method: "syntaxTree/visualizing",
93+
id: id,
94+
params: {
95+
textDocument: {
96+
uri: uri
97+
}
98+
}
99+
}
100+
end
101+
end
102+
103+
def test_formatting
104+
messages = [
105+
Initialize.new(1),
106+
TextDocumentDidOpen.new("file:///path/to/file.rb", "class Foo; end"),
107+
TextDocumentDidChange.new("file:///path/to/file.rb", "class Bar; end"),
108+
TextDocumentFormatting.new(2, "file:///path/to/file.rb"),
109+
TextDocumentDidClose.new("file:///path/to/file.rb"),
110+
Shutdown.new
111+
]
112+
113+
case run_server(messages)
114+
in { id: 1, result: { capabilities: Hash } },
115+
{ id: 2, result: [{ newText: new_text }] }
116+
assert_equal("class Bar\nend\n", new_text)
117+
end
118+
end
119+
120+
def test_inlay_hints
121+
messages = [
122+
Initialize.new(1),
123+
TextDocumentDidOpen.new("file:///path/to/file.rb", <<~RUBY),
124+
begin
125+
1 + 2 * 3
126+
rescue
127+
end
128+
RUBY
129+
TextDocumentInlayHints.new(2, "file:///path/to/file.rb"),
130+
Shutdown.new
131+
]
132+
133+
case run_server(messages)
134+
in { id: 1, result: { capabilities: Hash } },
135+
{ id: 2, result: { before:, after: } }
136+
assert_equal(1, before.length)
137+
assert_equal(2, after.length)
138+
end
139+
end
140+
141+
def test_visualizing
142+
messages = [
143+
Initialize.new(1),
144+
TextDocumentDidOpen.new("file:///path/to/file.rb", "1 + 2"),
145+
SyntaxTreeVisualizing.new(2, "file:///path/to/file.rb"),
146+
Shutdown.new
147+
]
148+
149+
case run_server(messages)
150+
in { id: 1, result: { capabilities: Hash } }, { id: 2, result: }
151+
assert_equal(
152+
"(program (statements ((binary (int \"1\") + (int \"2\")))))\n",
153+
result
154+
)
155+
end
156+
end
157+
158+
def test_reading_file
159+
Tempfile.open(%w[test- .rb]) do |file|
160+
file.write("class Foo; end")
161+
file.rewind
162+
163+
messages = [
164+
Initialize.new(1),
165+
TextDocumentFormatting.new(2, "file://#{file.path}"),
166+
Shutdown.new
167+
]
168+
169+
case run_server(messages)
170+
in { id: 1, result: { capabilities: Hash } },
171+
{ id: 2, result: [{ newText: new_text }] }
172+
assert_equal("class Foo\nend\n", new_text)
173+
end
174+
end
175+
end
176+
177+
private
178+
179+
def write(content)
180+
request = content.to_hash.merge(jsonrpc: "2.0").to_json
181+
"Content-Length: #{request.bytesize}\r\n\r\n#{request}"
182+
end
183+
184+
def read(content)
185+
[].tap do |messages|
186+
while (headers = content.gets("\r\n\r\n"))
187+
source = content.read(headers[/Content-Length: (\d+)/i, 1].to_i)
188+
messages << JSON.parse(source, symbolize_names: true)
189+
end
190+
end
191+
end
192+
193+
def run_server(messages)
194+
input = StringIO.new(messages.map { |message| write(message) }.join)
195+
output = StringIO.new
196+
197+
LanguageServer.new(input: input, output: output).run
198+
read(output.tap(&:rewind))
199+
end
200+
end
201+
end

0 commit comments

Comments
 (0)