diff --git a/lib/ruby_lsp/base_server.rb b/lib/ruby_lsp/base_server.rb index 2dfdaae49..92f74b624 100644 --- a/lib/ruby_lsp/base_server.rb +++ b/lib/ruby_lsp/base_server.rb @@ -91,7 +91,8 @@ def start # The following requests need to be executed in the main thread directly to avoid concurrency issues. Everything # else is pushed into the incoming queue case method - when "initialize", "initialized", "textDocument/didOpen", "textDocument/didClose", "textDocument/didChange" + when "initialize", "initialized", "textDocument/didOpen", "textDocument/didClose", "textDocument/didChange", + "$/cancelRequest" process_message(message) when "shutdown" send_log_message("Shutting down Ruby LSP...") @@ -133,6 +134,12 @@ def pop_response @outgoing_queue.pop end + # This method is only intended to be used in tests! Pushes a message to the incoming queue directly + sig { params(message: T::Hash[Symbol, T.untyped]).void } + def push_message(message) + @incoming_queue << message + end + sig { abstract.params(message: T::Hash[Symbol, T.untyped]).void } def process_message(message); end @@ -154,7 +161,11 @@ def new_worker # Check if the request was cancelled before trying to process it @mutex.synchronize do if id && @cancelled_requests.include?(id) - send_message(Result.new(id: id, response: nil)) + send_message(Error.new( + id: id, + code: Constant::ErrorCodes::REQUEST_CANCELLED, + message: "Request #{id} was cancelled", + )) @cancelled_requests.delete(id) next end diff --git a/lib/ruby_lsp/utils.rb b/lib/ruby_lsp/utils.rb index 4942d603a..325f28211 100644 --- a/lib/ruby_lsp/utils.rb +++ b/lib/ruby_lsp/utils.rb @@ -173,6 +173,9 @@ class Error sig { returns(String) } attr_reader :message + sig { returns(Integer) } + attr_reader :code + sig { params(id: Integer, code: Integer, message: String, data: T.nilable(T::Hash[Symbol, T.untyped])).void } def initialize(id:, code:, message:, data: nil) @id = id diff --git a/test/server_test.rb b/test/server_test.rb index 66f77c198..ba042c199 100644 --- a/test/server_test.rb +++ b/test/server_test.rb @@ -744,6 +744,51 @@ def test_requests_to_a_non_existing_position_return_error assert_match("Request textDocument/completion failed to find the target position.", error.message) end + def test_cancelling_requests_returns_expected_error_code + uri = URI("file:///foo.rb") + + @server.process_message({ + method: "textDocument/didOpen", + params: { + textDocument: { + uri: uri, + text: "class Foo\nend", + version: 1, + languageId: "ruby", + }, + }, + }) + + mutex = Mutex.new + mutex.lock + + # Use a mutex to lock the request in the middle so that we can cancel it before it finishes + @server.stubs(:text_document_definition) do |_message| + mutex.synchronize { 123 } + end + + thread = Thread.new do + @server.push_message({ + id: 1, + method: "textDocument/definition", + params: { + textDocument: { + uri: uri, + }, + position: { line: 0, character: 6 }, + }, + }) + end + + @server.process_message({ method: "$/cancelRequest", params: { id: 1 } }) + mutex.unlock + thread.join + + error = find_message(RubyLsp::Error) + assert_equal(RubyLsp::Constant::ErrorCodes::REQUEST_CANCELLED, error.code) + assert_equal("Request 1 was cancelled", error.message) + end + private def with_uninstalled_rubocop(&block)