Skip to content

Commit 7fe045a

Browse files
authored
Allow server to send asynchronous notifications to client (#559)
Previously, we were only allowing the server and its runtime add-ons to send simple strings to the stderr pipe, which would become log notifications. To support initiating progress from the server, we will need to allow any type of LSP notification to be send asynchronously. This PR changes our current logger thread to become a more general notifier thread, which will read any messages received from the server and push them to the outgoing queue. To prevent creating a breaking change, I kept the `log_message` method, but changed it to build the right notification expected.
1 parent 6279b37 commit 7fe045a

File tree

4 files changed

+78
-24
lines changed

4 files changed

+78
-24
lines changed

lib/ruby_lsp/ruby_lsp_rails/runner_client.rb

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,16 @@ def initialize(outgoing_queue)
100100
end
101101
end
102102

103-
@logger_thread = T.let(
103+
# Responsible for transmitting notifications coming from the server to the outgoing queue, so that we can do
104+
# things such as showing progress notifications initiated by the server
105+
@notifier_thread = T.let(
104106
Thread.new do
105-
while (content = @stderr.gets("\n"))
106-
log_message(content, type: RubyLsp::Constant::MessageType::LOG)
107+
until @stderr.closed?
108+
notification = read_notification
109+
110+
unless @outgoing_queue.closed? || !notification
111+
@outgoing_queue << notification
112+
end
107113
end
108114
rescue IOError
109115
# The server was shutdown and stderr is already closed
@@ -338,6 +344,21 @@ def read_content_length
338344

339345
length.to_i
340346
end
347+
348+
# Read a server notification from stderr. Only intended to be used by notifier thread
349+
sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) }
350+
def read_notification
351+
headers = @stderr.gets("\r\n\r\n")
352+
return unless headers
353+
354+
length = headers[/Content-Length: (\d+)/i, 1]
355+
return unless length
356+
357+
raw_content = @stderr.read(length.to_i)
358+
return unless raw_content
359+
360+
JSON.parse(raw_content, symbolize_names: true)
361+
end
341362
end
342363

343364
class NullClient < RunnerClient

lib/ruby_lsp/ruby_lsp_rails/server.rb

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,10 @@
77
module RubyLsp
88
module Rails
99
module Common
10-
# Write a message to the client. Can be used for sending notifications to the editor
11-
def send_message(message)
12-
json_message = message.to_json
13-
@stdout.write("Content-Length: #{json_message.bytesize}\r\n\r\n#{json_message}")
14-
end
15-
16-
# Log a message to the editor's output panel
17-
def log_message(message)
18-
$stderr.puts(message)
10+
# Log a message to the editor's output panel. The type is the number of the message type, which can be found in
11+
# the specification https://microsoft.github.io/language-server-protocol/specification/#messageType
12+
def log_message(message, type: 4)
13+
send_notification({ method: "window/logMessage", params: { type: type, message: message } })
1914
end
2015

2116
# Sends an error result to a request, if the request failed. DO NOT INVOKE THIS METHOD FOR NOTIFICATIONS! Use
@@ -54,6 +49,20 @@ def with_notification_error_handling(notification_name, &block)
5449
rescue => e
5550
log_message("Request #{notification_name} failed:\n#{e.full_message(highlight: false)}")
5651
end
52+
53+
private
54+
55+
# Write a response message back to the client
56+
def send_message(message)
57+
json_message = message.to_json
58+
@stdout.write("Content-Length: #{json_message.bytesize}\r\n\r\n#{json_message}")
59+
end
60+
61+
# Write a notification to the client to be transmitted to the editor
62+
def send_notification(message)
63+
json_message = message.to_json
64+
@stderr.write("Content-Length: #{json_message.bytesize}\r\n\r\n#{json_message}")
65+
end
5766
end
5867

5968
class ServerAddon
@@ -76,16 +85,17 @@ def delegate(name, request, params)
7685
end
7786

7887
# Instantiate all server addons and store them in a hash for easy access after we have discovered the classes
79-
def finalize_registrations!(stdout)
88+
def finalize_registrations!(stdout, stderr)
8089
until @server_addon_classes.empty?
81-
addon = @server_addon_classes.shift.new(stdout)
90+
addon = @server_addon_classes.shift.new(stdout, stderr)
8291
@server_addons[addon.name] = addon
8392
end
8493
end
8594
end
8695

87-
def initialize(stdout)
96+
def initialize(stdout, stderr)
8897
@stdout = stdout
98+
@stderr = stderr
8999
end
90100

91101
def name
@@ -100,11 +110,11 @@ def execute(request, params)
100110
class Server
101111
include Common
102112

103-
def initialize(stdout: $stdout, override_default_output_device: true)
113+
def initialize(stdout: $stdout, stderr: $stderr, override_default_output_device: true)
104114
# Grab references to the original pipes so that we can change the default output device further down
105115
@stdin = $stdin
106116
@stdout = stdout
107-
@stderr = $stderr
117+
@stderr = stderr
108118
@stdin.sync = true
109119
@stdout.sync = true
110120
@stderr.sync = true
@@ -169,7 +179,7 @@ def execute(request, params)
169179
when "server_addon/register"
170180
with_notification_error_handling(request) do
171181
require params[:server_addon_path]
172-
ServerAddon.finalize_registrations!(@stdout)
182+
ServerAddon.finalize_registrations!(@stdout, @stderr)
173183
end
174184
when "server_addon/delegate"
175185
server_addon_name = params[:server_addon_name]

test/ruby_lsp_rails/runner_client_test.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ def name
128128
129129
def execute(request, params)
130130
log_message("Hello!")
131-
send_message({ request:, params: })
131+
send_result({ request: request, params: params })
132132
end
133133
end
134134
RUBY
@@ -141,16 +141,16 @@ def execute(request, params)
141141
# Finished booting server
142142
pop_log_notification(@outgoing_queue, RubyLsp::Constant::MessageType::LOG)
143143

144-
log = pop_log_notification(@outgoing_queue, RubyLsp::Constant::MessageType::LOG)
144+
log = @outgoing_queue.pop
145145

146146
# Sometimes we get warnings concerning deprecations and they mess up this expectation
147147
3.times do
148-
unless log.params.message.match?(/Hello!/)
149-
log = pop_log_notification(@outgoing_queue, RubyLsp::Constant::MessageType::LOG)
148+
unless log.dig(:params, :message).match?(/Hello!/)
149+
log = @outgoing_queue.pop
150150
end
151151
end
152152

153-
assert_match("Hello!", log.params.message)
153+
assert_match("Hello!", log.dig(:params, :message))
154154
ensure
155155
FileUtils.rm("server_addon.rb")
156156
end

test/ruby_lsp_rails/server_test.rb

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
class ServerTest < ActiveSupport::TestCase
88
setup do
99
@stdout = StringIO.new
10-
@server = RubyLsp::Rails::Server.new(stdout: @stdout, override_default_output_device: false)
10+
@stderr = StringIO.new
11+
@server = RubyLsp::Rails::Server.new(stdout: @stdout, stderr: @stderr, override_default_output_device: false)
1112
end
1213

1314
test "returns nil if model doesn't exist" do
@@ -229,6 +230,28 @@ def resolve_route_info(requirements)
229230
assert_equal expected, @stdout.string
230231
end
231232

233+
test "log_message sends notification to client" do
234+
@server.log_message("Hello")
235+
236+
expected_notification = {
237+
method: "window/logMessage",
238+
params: { type: 4, message: "Hello" },
239+
}.to_json
240+
241+
assert_equal "Content-Length: #{expected_notification.bytesize}\r\n\r\n#{expected_notification}", @stderr.string
242+
end
243+
244+
test "log_message allows server to define message type" do
245+
@server.log_message("Hello", type: 1)
246+
247+
expected_notification = {
248+
method: "window/logMessage",
249+
params: { type: 1, message: "Hello" },
250+
}.to_json
251+
252+
assert_equal "Content-Length: #{expected_notification.bytesize}\r\n\r\n#{expected_notification}", @stderr.string
253+
end
254+
232255
private
233256

234257
def response

0 commit comments

Comments
 (0)