From 1f75279aa878cba316b4263e143db8310e45007c Mon Sep 17 00:00:00 2001 From: Justin Searls Date: Mon, 6 Feb 2023 11:36:34 -0500 Subject: [PATCH 01/13] Add testDocument/diagnostic which is what VSCode needs to show problems. --- lib/standard/lsp/server.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/standard/lsp/server.rb b/lib/standard/lsp/server.rb index 7c2839c3..a8dcbce9 100644 --- a/lib/standard/lsp/server.rb +++ b/lib/standard/lsp/server.rb @@ -39,6 +39,12 @@ def initialize(standardizer) exit }, + "textDocument/diagnostic" => ->(request) { + td = request[:params][:textDocument] + result = diagnostic(td[:uri], td[:text]) + writer.write(result) + }, + "textDocument/didChange" => ->(request) { params = request[:params] result = diagnostic(params[:textDocument][:uri], params[:contentChanges][0][:text]) From 92ac85506b5670ed844f163f8fc62ffb5aa2bf87 Mon Sep 17 00:00:00 2001 From: Justin Searls Date: Mon, 6 Feb 2023 11:37:01 -0500 Subject: [PATCH 02/13] The spec says thou must respond with an error code when not responding to $/ commands like $/cancelRequest --- lib/standard/lsp/server.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/standard/lsp/server.rb b/lib/standard/lsp/server.rb index a8dcbce9..817b8968 100644 --- a/lib/standard/lsp/server.rb +++ b/lib/standard/lsp/server.rb @@ -76,7 +76,11 @@ def start if (subscriber = subscribers[method]) subscriber.call(request) else - logger.puts "unknown method: #{method}" + writer.write({id: request[:id], error: Proto::Interface::ResponseError.new( + code: Proto::Constant::ErrorCodes::METHOD_NOT_FOUND, + message: "Unsupported Method: #{method}" + )}) + logger.puts "Unsupported Method: #{method}" end rescue => e logger.puts "error #{e.class} #{e.message[0..100]}" From cb48e11a5b32662ab588d32dd6f13803ad81b59b Mon Sep 17 00:00:00 2001 From: Justin Searls Date: Mon, 6 Feb 2023 12:33:32 -0500 Subject: [PATCH 03/13] VS Code will not consider a server gracefully stopped if it doesn't respond with a null result --- lib/standard/lsp/server.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/standard/lsp/server.rb b/lib/standard/lsp/server.rb index 817b8968..bf1b7708 100644 --- a/lib/standard/lsp/server.rb +++ b/lib/standard/lsp/server.rb @@ -35,8 +35,11 @@ def initialize(standardizer) "initialized" => ->(request) { logger.puts "standard v#{Standard::VERSION} initialized, pid #{Process.pid}" }, "shutdown" => ->(request) { - logger.puts "asked to shutdown, exiting..." - exit + logger.puts "Asked to shutdown Standard LSP server. Exiting..." + at_exit { + writer.write(id: request[:id], result: nil) + } + exit 0 }, "textDocument/diagnostic" => ->(request) { From f54277b155d32fe771867d2bc18bd9be132e8f93 Mon Sep 17 00:00:00 2001 From: Justin Searls Date: Wed, 8 Feb 2023 16:06:24 -0500 Subject: [PATCH 04/13] tweak server --- lib/standard/lsp/server.rb | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/standard/lsp/server.rb b/lib/standard/lsp/server.rb index bf1b7708..fffda590 100644 --- a/lib/standard/lsp/server.rb +++ b/lib/standard/lsp/server.rb @@ -26,16 +26,18 @@ def initialize(standardizer) capabilities: Proto::Interface::ServerCapabilities.new( document_formatting_provider: true, diagnostic_provider: true, - text_document_sync: Proto::Constant::TextDocumentSyncKind::FULL + text_document_sync: Proto::Interface::TextDocumentSyncOptions.new( + change: Proto::Constant::TextDocumentSyncKind::FULL + ) ) ) writer.write(id: request[:id], result: init_result) }, - "initialized" => ->(request) { logger.puts "standard v#{Standard::VERSION} initialized, pid #{Process.pid}" }, + "initialized" => ->(request) { logger.puts "Standard Ruby v#{Standard::VERSION} LSP server initialized, pid #{Process.pid}" }, "shutdown" => ->(request) { - logger.puts "Asked to shutdown Standard LSP server. Exiting..." + logger.puts "Client asked to shutdown Standard LSP server. Exiting..." at_exit { writer.write(id: request[:id], result: nil) } @@ -69,7 +71,9 @@ def initialize(standardizer) writer.write({id: request[:id], result: format_file(uri)}) }, - "textDocument/didSave" => ->(request) {} + "textDocument/didSave" => ->(request) {}, + + "$/cancelRequest" => ->(request) {} } end @@ -86,7 +90,7 @@ def start logger.puts "Unsupported Method: #{method}" end rescue => e - logger.puts "error #{e.class} #{e.message[0..100]}" + logger.puts "Error #{e.class} #{e.message[0..100]}" logger.puts e.backtrace.inspect end end @@ -128,7 +132,7 @@ def diagnostic(file_uri, text) when "refactor", "info" SEV::HINT else # the above cases fully cover what RuboCop sends at this time - logger.puts "unknown severity: #{severity.inspect}" + logger.puts "Unknown severity: #{severity.inspect}" SEV::HINT end From f35aa3eeaf3635ae6037cf41cbeccda8d98ecabf Mon Sep 17 00:00:00 2001 From: Justin Searls Date: Wed, 8 Feb 2023 17:36:49 -0500 Subject: [PATCH 05/13] Note unsupported messages --- lib/standard/lsp/server.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/standard/lsp/server.rb b/lib/standard/lsp/server.rb index fffda590..4d0c12b6 100644 --- a/lib/standard/lsp/server.rb +++ b/lib/standard/lsp/server.rb @@ -71,9 +71,10 @@ def initialize(standardizer) writer.write({id: request[:id], result: format_file(uri)}) }, + # Unsupported/no-op commands "textDocument/didSave" => ->(request) {}, - - "$/cancelRequest" => ->(request) {} + "$/cancelRequest" => ->(request) {}, + "$/setTrace" => ->(request) {} } end From cde78b2db0bdc3d3e124d1e48d7be46fe6aa7bf4 Mon Sep 17 00:00:00 2001 From: Justin Searls Date: Wed, 8 Feb 2023 17:37:06 -0500 Subject: [PATCH 06/13] If asked to format a file that didn't sync, just log instead --- lib/standard/lsp/server.rb | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/lib/standard/lsp/server.rb b/lib/standard/lsp/server.rb index 4d0c12b6..884995c7 100644 --- a/lib/standard/lsp/server.rb +++ b/lib/standard/lsp/server.rb @@ -98,18 +98,22 @@ def start def format_file(file_uri) text = text_cache[file_uri] - new_text = standardizer.format(text) - - if new_text == text + if text.nil? + logger.puts "Format request arrived before text synchonized; skipping format of: `#{file_uri}'" [] else - [{ - newText: new_text, - range: { - start: {line: 0, character: 0}, - end: {line: text.count("\n") + 1, character: 0} - } - }] + new_text = standardizer.format(text) + if new_text == text + [] + else + [{ + newText: new_text, + range: { + start: {line: 0, character: 0}, + end: {line: text.count("\n") + 1, character: 0} + } + }] + end end end From f094cb2f707e51d0568e0aab46a824938a62d64d Mon Sep 17 00:00:00 2001 From: Justin Searls Date: Wed, 8 Feb 2023 20:33:57 -0500 Subject: [PATCH 07/13] switch to Mocktail --- Gemfile | 2 +- Gemfile.lock | 4 ++-- test/standard/creates_config_store_test.rb | 16 ++++++++-------- test/test_helper.rb | 5 +++-- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/Gemfile b/Gemfile index f80257b7..21d2fa29 100644 --- a/Gemfile +++ b/Gemfile @@ -6,7 +6,7 @@ gem "bundler" gem "minitest", "~> 5.0" gem "pry" gem "rake", "~> 13.0" -gem "gimme" +gem "mocktail" if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.5") gem "simplecov" diff --git a/Gemfile.lock b/Gemfile.lock index 8adfee90..e54c1b96 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -12,11 +12,11 @@ GEM ast (2.4.2) coderay (1.1.3) docile (1.4.0) - gimme (0.5.0) json (2.6.3) language_server-protocol (3.17.0.3) method_source (1.0.0) minitest (5.17.0) + mocktail (1.2.2) parallel (1.22.1) parser (3.2.0.0) ast (~> 2.4.1) @@ -57,8 +57,8 @@ PLATFORMS DEPENDENCIES bundler - gimme minitest (~> 5.0) + mocktail pry rake (~> 13.0) simplecov diff --git a/test/standard/creates_config_store_test.rb b/test/standard/creates_config_store_test.rb index 40406dbe..f563c370 100644 --- a/test/standard/creates_config_store_test.rb +++ b/test/standard/creates_config_store_test.rb @@ -3,10 +3,10 @@ class Standard::CreatesConfigStore class Test < UnitTest def setup - @assigns_rubocop_yaml = gimme_next(AssignsRubocopYaml) - @sets_target_ruby_version = gimme_next(SetsTargetRubyVersion) - @configures_ignored_paths = gimme_next(ConfiguresIgnoredPaths) - @merges_user_config_extensions = gimme_next(MergesUserConfigExtensions) + @assigns_rubocop_yaml = Mocktail.of_next(AssignsRubocopYaml) + @sets_target_ruby_version = Mocktail.of_next(SetsTargetRubyVersion) + @configures_ignored_paths = Mocktail.of_next(ConfiguresIgnoredPaths) + @merges_user_config_extensions = Mocktail.of_next(MergesUserConfigExtensions) @subject = Standard::CreatesConfigStore.new end @@ -14,13 +14,13 @@ def setup def test_minimal_config standard_config = :some_config options_config = :some_options - give(@assigns_rubocop_yaml).call(is_a(RuboCop::ConfigStore), standard_config) { options_config } + stubs { |m| @assigns_rubocop_yaml.call(m.is_a(RuboCop::ConfigStore), standard_config) }.with { options_config } @subject.call(standard_config) - verify(@sets_target_ruby_version).call(options_config, standard_config) - verify(@configures_ignored_paths).call(options_config, standard_config) - verify(@merges_user_config_extensions).call(options_config, standard_config) + verify { @sets_target_ruby_version.call(options_config, standard_config) } + verify { @configures_ignored_paths.call(options_config, standard_config) } + verify { @merges_user_config_extensions.call(options_config, standard_config) } end end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 8de4be9d..e855eeff 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -10,11 +10,12 @@ $LOAD_PATH << "test" require "standard" -require "gimme" +require "mocktail" require "minitest/autorun" require "pry" class UnitTest < Minitest::Test + include Mocktail::DSL make_my_diffs_pretty! def self.path(relative) @@ -22,7 +23,7 @@ def self.path(relative) end def teardown - Gimme.reset + Mocktail.reset end protected From 136d265a1a1d6b55d7c6494afd38b0466f54cc02 Mon Sep 17 00:00:00 2001 From: Justin Searls Date: Wed, 8 Feb 2023 20:47:24 -0500 Subject: [PATCH 08/13] update test --- lib/standard/lsp/server.rb | 2 +- test/standard/runners/lsp_test.rb | 103 +++++++++++++++++++++++++++++- 2 files changed, 102 insertions(+), 3 deletions(-) diff --git a/lib/standard/lsp/server.rb b/lib/standard/lsp/server.rb index 884995c7..ee4db704 100644 --- a/lib/standard/lsp/server.rb +++ b/lib/standard/lsp/server.rb @@ -99,7 +99,7 @@ def start def format_file(file_uri) text = text_cache[file_uri] if text.nil? - logger.puts "Format request arrived before text synchonized; skipping format of: `#{file_uri}'" + logger.puts "Format request arrived before text synchonized; skipping: `#{file_uri}'" [] else new_text = standardizer.format(text) diff --git a/test/standard/runners/lsp_test.rb b/test/standard/runners/lsp_test.rb index 154611c9..951ef772 100644 --- a/test/standard/runners/lsp_test.rb +++ b/test/standard/runners/lsp_test.rb @@ -16,7 +16,7 @@ def test_server_initializes_and_responds_with_proper_capabilities assert_equal msgs.first, { id: 2, result: {capabilities: { - textDocumentSync: 1, + textDocumentSync: {change: 1}, documentFormattingProvider: true, diagnosticProvider: true }}, @@ -24,7 +24,7 @@ def test_server_initializes_and_responds_with_proper_capabilities } end - def test_get_diagnostics + def test_did_open msgs, err = run_server_on_requests({ method: "textDocument/didOpen", jsonrpc: "2.0", @@ -66,6 +66,48 @@ def test_get_diagnostics }, msgs.first) end + def test_diagnotic_route + msgs, err = run_server_on_requests({ + method: "textDocument/diagnostic", + jsonrpc: "2.0", + params: { + textDocument: { + languageId: "ruby", + text: "def hi\n [1, 2,\n 3 ]\nend\n", + uri: "file:///path/to/file.rb", + version: 0 + } + } + }) + + assert_equal "", err.string + assert_equal 1, msgs.count + assert_equal({ + method: "textDocument/publishDiagnostics", + params: { + diagnostics: [ + {code: "Layout/ArrayAlignment", + message: "Use one level of indentation for elements following the first line of a multi-line array.", + range: {start: {character: 3, line: 2}, end: {character: 3, line: 2}}, + severity: 3, + source: "standard"}, + {code: "Layout/ExtraSpacing", + message: "Unnecessary spacing detected.", + range: {start: {character: 4, line: 2}, end: {character: 4, line: 2}}, + severity: 3, + source: "standard"}, + {code: "Layout/SpaceInsideArrayLiteralBrackets", + message: "Do not use space inside array brackets.", + range: {start: {character: 4, line: 2}, end: {character: 5, line: 2}}, + severity: 3, + source: "standard"} + ], + uri: "file:///path/to/file.rb" + }, + jsonrpc: "2.0" + }, msgs.first) + end + def test_format msgs, err = run_server_on_requests( { @@ -120,6 +162,63 @@ def test_format ) end + def test_unsupported_commands + _, err = run_server_on_requests( + { + method: "$/cancelRequest", + id: 1, + jsonrpc: "2.0", + params: {} + }, + { + method: "$/setTrace", + id: 1, + jsonrpc: "2.0", + params: {} + } + ) + + assert_empty err.string + end + + def test_initialized + _, err = run_server_on_requests( + { + method: "initialized", + id: 1, + jsonrpc: "2.0", + params: {} + } + ) + + assert_match(/Standard Ruby v\d+.\d+.\d+ LSP server initialized, pid \d+/, err.string) + end + + def test_format_with_unsynced_file + msgs, err = run_server_on_requests( + { + method: "textDocument/formatting", + id: 20, + jsonrpc: "2.0", + params: { + options: {insertSpaces: true, tabSize: 2}, + textDocument: {uri: "file:///path/to/file.rb"} + } + } + ) + + assert_equal "Format request arrived before text synchonized; skipping: `file:///path/to/file.rb'", err.string.chomp + format_result = msgs.last + assert_equal( + { + id: 20, + result: [], + jsonrpc: "2.0" + }, + format_result + ) + end + private def run_server_on_requests(*requests) From 4da08f9626dee9a5b689a52470be833f9c7e60ac Mon Sep 17 00:00:00 2001 From: Justin Searls Date: Thu, 9 Feb 2023 09:46:24 -0500 Subject: [PATCH 09/13] Separate server and route handling --- lib/standard/lsp/routes.rb | 152 +++++++++++++++++++++++++++++ lib/standard/lsp/server.rb | 158 ++++--------------------------- lib/standard/lsp/standardizer.rb | 2 +- lib/standard/runners/lsp.rb | 4 +- 4 files changed, 172 insertions(+), 144 deletions(-) create mode 100644 lib/standard/lsp/routes.rb diff --git a/lib/standard/lsp/routes.rb b/lib/standard/lsp/routes.rb new file mode 100644 index 00000000..047db28c --- /dev/null +++ b/lib/standard/lsp/routes.rb @@ -0,0 +1,152 @@ +module Standard + module Lsp + class Routes + def initialize(writer, logger, standardizer) + @writer = writer + @logger = logger + @standardizer = standardizer + + @text_cache = {} + end + + def self.handle(name, &block) + define_method("handle_#{name}", &block) + end + + def for(name) + method("handle_#{name}") + end + + handle "initialize" do |request| + @writer.write(id: request[:id], result: Proto::Interface::InitializeResult.new( + capabilities: Proto::Interface::ServerCapabilities.new( + document_formatting_provider: true, + diagnostic_provider: true, + text_document_sync: Proto::Interface::TextDocumentSyncOptions.new( + change: Proto::Constant::TextDocumentSyncKind::FULL + ) + ) + )) + end + + handle "initialized" do |request| + @logger.puts "Standard Ruby v#{Standard::VERSION} LSP server initialized, pid #{Process.pid}" + end + + handle "shutdown" do |request| + @logger.puts "Client asked to shutdown Standard LSP server. Exiting..." + at_exit { + @writer.write(id: request[:id], result: nil) + } + exit 0 + end + + handle "textDocument/diagnostic" do |request| + doc = request[:params][:textDocument] + result = diagnostic(doc[:uri], doc[:text]) + @writer.write(result) + end + + handle "textDocument/didChange" do |request| + params = request[:params] + result = diagnostic(params[:textDocument][:uri], params[:contentChanges][0][:text]) + @writer.write(result) + end + + handle "textDocument/didOpen" do |request| + doc = request[:params][:textDocument] + result = diagnostic(doc[:uri], doc[:text]) + @writer.write(result) + end + + handle "textDocument/didClose" do |request| + @text_cache.delete(request.dig(:params, :textDocument, :uri)) + end + + handle "textDocument/formatting" do |request| + uri = request[:params][:textDocument][:uri] + @writer.write({id: request[:id], result: format_file(uri)}) + end + + handle "textDocument/didSave" do |_request| + # No-op + end + + handle "$/cancelRequest" do |_request| + # No-op + end + + handle "$/setTrace" do |_request| + # No-op + end + + private + + def format_file(file_uri) + text = @text_cache[file_uri] + if text.nil? + @logger.puts "Format request arrived before text synchonized; skipping: `#{file_uri}'" + [] + else + new_text = @standardizer.format(text) + if new_text == text + [] + else + [{ + newText: new_text, + range: { + start: {line: 0, character: 0}, + end: {line: text.count("\n") + 1, character: 0} + } + }] + end + end + end + + def diagnostic(file_uri, text) + @text_cache[file_uri] = text + offenses = @standardizer.offenses(text) + + lsp_diagnostics = offenses.map { |o| + code = o[:cop_name] + + msg = o[:message].delete_prefix(code) + loc = o[:location] + + severity = case o[:severity] + when "error", "fatal" + SEV::ERROR + when "warning" + SEV::WARNING + when "convention" + SEV::INFORMATION + when "refactor", "info" + SEV::HINT + else # the above cases fully cover what RuboCop sends at this time + logger.puts "Unknown severity: #{severity.inspect}" + SEV::HINT + end + + { + code: code, + message: msg, + range: { + start: {character: loc[:start_column] - 1, line: loc[:start_line] - 1}, + end: {character: loc[:last_column] - 1, line: loc[:last_line] - 1} + }, + severity: severity, + source: "standard" + } + } + + { + method: "textDocument/publishDiagnostics", + params: { + uri: file_uri, + diagnostics: lsp_diagnostics + } + } + end + end + end +end diff --git a/lib/standard/lsp/server.rb b/lib/standard/lsp/server.rb index ee4db704..2174cc4a 100644 --- a/lib/standard/lsp/server.rb +++ b/lib/standard/lsp/server.rb @@ -1,166 +1,42 @@ require "language_server-protocol" require_relative "standardizer" +require_relative "routes" module Standard - module LSP - class Server - Proto = LanguageServer::Protocol - SEV = Proto::Constant::DiagnosticSeverity + module Lsp + Proto = LanguageServer::Protocol + SEV = Proto::Constant::DiagnosticSeverity + class Server def self.start(standardizer) new(standardizer).start end - attr_accessor :standardizer, :writer, :reader, :logger, :text_cache, :subscribers - def initialize(standardizer) - self.standardizer = standardizer - self.writer = Proto::Transport::Io::Writer.new($stdout) - self.reader = Proto::Transport::Io::Reader.new($stdin) - self.logger = $stderr - self.text_cache = {} - - self.subscribers = { - "initialize" => ->(request) { - init_result = Proto::Interface::InitializeResult.new( - capabilities: Proto::Interface::ServerCapabilities.new( - document_formatting_provider: true, - diagnostic_provider: true, - text_document_sync: Proto::Interface::TextDocumentSyncOptions.new( - change: Proto::Constant::TextDocumentSyncKind::FULL - ) - ) - ) - writer.write(id: request[:id], result: init_result) - }, - - "initialized" => ->(request) { logger.puts "Standard Ruby v#{Standard::VERSION} LSP server initialized, pid #{Process.pid}" }, - - "shutdown" => ->(request) { - logger.puts "Client asked to shutdown Standard LSP server. Exiting..." - at_exit { - writer.write(id: request[:id], result: nil) - } - exit 0 - }, - - "textDocument/diagnostic" => ->(request) { - td = request[:params][:textDocument] - result = diagnostic(td[:uri], td[:text]) - writer.write(result) - }, - - "textDocument/didChange" => ->(request) { - params = request[:params] - result = diagnostic(params[:textDocument][:uri], params[:contentChanges][0][:text]) - writer.write(result) - }, - - "textDocument/didOpen" => ->(request) { - td = request[:params][:textDocument] - result = diagnostic(td[:uri], td[:text]) - writer.write(result) - }, - - "textDocument/didClose" => ->(request) { - text_cache.delete(request.dig(:params, :textDocument, :uri)) - }, - - "textDocument/formatting" => ->(request) { - uri = request[:params][:textDocument][:uri] - writer.write({id: request[:id], result: format_file(uri)}) - }, - - # Unsupported/no-op commands - "textDocument/didSave" => ->(request) {}, - "$/cancelRequest" => ->(request) {}, - "$/setTrace" => ->(request) {} - } + @standardizer = standardizer + @writer = Proto::Transport::Io::Writer.new($stdout) + @reader = Proto::Transport::Io::Reader.new($stdin) + @logger = $stderr + @routes = Routes.new(@writer, @logger, @standardizer) end def start - reader.read do |request| + @reader.read do |request| method = request[:method] - if (subscriber = subscribers[method]) - subscriber.call(request) + if (route = @routes.for(method)) + route.call(request) else - writer.write({id: request[:id], error: Proto::Interface::ResponseError.new( + @writer.write({id: request[:id], error: Proto::Interface::ResponseError.new( code: Proto::Constant::ErrorCodes::METHOD_NOT_FOUND, message: "Unsupported Method: #{method}" )}) - logger.puts "Unsupported Method: #{method}" + @logger.puts "Unsupported Method: #{method}" end rescue => e - logger.puts "Error #{e.class} #{e.message[0..100]}" - logger.puts e.backtrace.inspect + @logger.puts "Error #{e.class} #{e.message[0..100]}" + @logger.puts e.backtrace.inspect end end - - def format_file(file_uri) - text = text_cache[file_uri] - if text.nil? - logger.puts "Format request arrived before text synchonized; skipping: `#{file_uri}'" - [] - else - new_text = standardizer.format(text) - if new_text == text - [] - else - [{ - newText: new_text, - range: { - start: {line: 0, character: 0}, - end: {line: text.count("\n") + 1, character: 0} - } - }] - end - end - end - - def diagnostic(file_uri, text) - text_cache[file_uri] = text - offenses = standardizer.offenses(text) - - lsp_diagnostics = offenses.map { |o| - code = o[:cop_name] - - msg = o[:message].delete_prefix(code) - loc = o[:location] - - severity = case o[:severity] - when "error", "fatal" - SEV::ERROR - when "warning" - SEV::WARNING - when "convention" - SEV::INFORMATION - when "refactor", "info" - SEV::HINT - else # the above cases fully cover what RuboCop sends at this time - logger.puts "Unknown severity: #{severity.inspect}" - SEV::HINT - end - - { - code: code, - message: msg, - range: { - start: {character: loc[:start_column] - 1, line: loc[:start_line] - 1}, - end: {character: loc[:last_column] - 1, line: loc[:last_line] - 1} - }, - severity: severity, - source: "standard" - } - } - - { - method: "textDocument/publishDiagnostics", - params: { - diagnostics: lsp_diagnostics, - uri: file_uri - } - } - end end end end diff --git a/lib/standard/lsp/standardizer.rb b/lib/standard/lsp/standardizer.rb index 3cde1877..2b9e2bfc 100644 --- a/lib/standard/lsp/standardizer.rb +++ b/lib/standard/lsp/standardizer.rb @@ -2,7 +2,7 @@ require "tempfile" module Standard - module LSP + module Lsp class Standardizer def initialize(config) @template_options = config diff --git a/lib/standard/runners/lsp.rb b/lib/standard/runners/lsp.rb index 0e82d259..4d91dba1 100644 --- a/lib/standard/runners/lsp.rb +++ b/lib/standard/runners/lsp.rb @@ -4,8 +4,8 @@ module Standard module Runners class Lsp def call(config) - standardizer = Standard::LSP::Standardizer.new(config) - Standard::LSP::Server.start(standardizer) + standardizer = Standard::Lsp::Standardizer.new(config) + Standard::Lsp::Server.start(standardizer) end end end From ce4d02f65249d381ff1c359c5c0d8ae6b86ee2b6 Mon Sep 17 00:00:00 2001 From: Justin Searls Date: Thu, 9 Feb 2023 09:46:36 -0500 Subject: [PATCH 10/13] add `m` gem for easier test runs --- Gemfile | 1 + Gemfile.lock | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/Gemfile b/Gemfile index 21d2fa29..d6295013 100644 --- a/Gemfile +++ b/Gemfile @@ -7,6 +7,7 @@ gem "minitest", "~> 5.0" gem "pry" gem "rake", "~> 13.0" gem "mocktail" +gem "m" if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.5") gem "simplecov" diff --git a/Gemfile.lock b/Gemfile.lock index e54c1b96..505cc663 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -14,6 +14,9 @@ GEM docile (1.4.0) json (2.6.3) language_server-protocol (3.17.0.3) + m (1.6.1) + method_source (>= 0.6.7) + rake (>= 0.9.2.2) method_source (1.0.0) minitest (5.17.0) mocktail (1.2.2) @@ -57,6 +60,7 @@ PLATFORMS DEPENDENCIES bundler + m minitest (~> 5.0) mocktail pry From 37fcd4af3c4fe2ba652c52434688f04f4ee1b09c Mon Sep 17 00:00:00 2001 From: Justin Searls Date: Thu, 9 Feb 2023 09:58:10 -0500 Subject: [PATCH 11/13] round out some coverage --- lib/standard/lsp/routes.rb | 5 ++- test/standard/runners/lsp_test.rb | 52 ++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/lib/standard/lsp/routes.rb b/lib/standard/lsp/routes.rb index 047db28c..f17ea182 100644 --- a/lib/standard/lsp/routes.rb +++ b/lib/standard/lsp/routes.rb @@ -14,7 +14,10 @@ def self.handle(name, &block) end def for(name) - method("handle_#{name}") + name = "handle_#{name}" + if respond_to?(name) + method(name) + end end handle "initialize" do |request| diff --git a/test/standard/runners/lsp_test.rb b/test/standard/runners/lsp_test.rb index 951ef772..7ab8c07a 100644 --- a/test/standard/runners/lsp_test.rb +++ b/test/standard/runners/lsp_test.rb @@ -162,7 +162,7 @@ def test_format ) end - def test_unsupported_commands + def test_no_op_commands _, err = run_server_on_requests( { method: "$/cancelRequest", @@ -196,6 +196,28 @@ def test_initialized def test_format_with_unsynced_file msgs, err = run_server_on_requests( + { + method: "textDocument/didOpen", + jsonrpc: "2.0", + params: { + textDocument: { + languageId: "ruby", + text: "def hi\n [1, 2,\n 3 ]\nend\n", + uri: "file:///path/to/file.rb", + version: 0 + } + } + }, + # didClose should cause the file to be unsynced + { + method: "textDocument/didClose", + jsonrpc: "2.0", + params: { + textDocument: { + uri: "file:///path/to/file.rb" + } + } + }, { method: "textDocument/formatting", id: 20, @@ -219,6 +241,34 @@ def test_format_with_unsynced_file ) end + def test_unknown_commands + msgs, err = run_server_on_requests( + { + id: 18, + method: "textDocument/didMassage", + jsonrpc: "2.0", + params: { + textDocument: { + languageId: "ruby", + text: "def hi\n [1, 2,\n 3 ]\nend\n", + uri: "file:///path/to/file.rb", + version: 0 + } + } + } + ) + + assert_equal "Unsupported Method: textDocument/didMassage", err.string.chomp + assert_equal({ + id: 18, + error: { + code: -32601, + message: "Unsupported Method: textDocument/didMassage" + }, + jsonrpc: "2.0" + }, msgs.last) + end + private def run_server_on_requests(*requests) From 7b44cd2da33143e960202c325eda4d0fbc6a3564 Mon Sep 17 00:00:00 2001 From: Justin Searls Date: Thu, 9 Feb 2023 11:57:49 -0500 Subject: [PATCH 12/13] Wrap logger and prefix messages --- lib/standard/lsp/logger.rb | 9 +++++++++ lib/standard/lsp/server.rb | 3 ++- test/standard/runners/lsp_test.rb | 4 ++-- 3 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 lib/standard/lsp/logger.rb diff --git a/lib/standard/lsp/logger.rb b/lib/standard/lsp/logger.rb new file mode 100644 index 00000000..3253f03b --- /dev/null +++ b/lib/standard/lsp/logger.rb @@ -0,0 +1,9 @@ +module Standard + module Lsp + class Logger + def puts(message) + warn("[server] #{message}") + end + end + end +end diff --git a/lib/standard/lsp/server.rb b/lib/standard/lsp/server.rb index 2174cc4a..ff85db61 100644 --- a/lib/standard/lsp/server.rb +++ b/lib/standard/lsp/server.rb @@ -1,6 +1,7 @@ require "language_server-protocol" require_relative "standardizer" require_relative "routes" +require_relative "logger" module Standard module Lsp @@ -16,7 +17,7 @@ def initialize(standardizer) @standardizer = standardizer @writer = Proto::Transport::Io::Writer.new($stdout) @reader = Proto::Transport::Io::Reader.new($stdin) - @logger = $stderr + @logger = Logger.new @routes = Routes.new(@writer, @logger, @standardizer) end diff --git a/test/standard/runners/lsp_test.rb b/test/standard/runners/lsp_test.rb index 7ab8c07a..0e4170e1 100644 --- a/test/standard/runners/lsp_test.rb +++ b/test/standard/runners/lsp_test.rb @@ -229,7 +229,7 @@ def test_format_with_unsynced_file } ) - assert_equal "Format request arrived before text synchonized; skipping: `file:///path/to/file.rb'", err.string.chomp + assert_equal "[server] Format request arrived before text synchonized; skipping: `file:///path/to/file.rb'", err.string.chomp format_result = msgs.last assert_equal( { @@ -258,7 +258,7 @@ def test_unknown_commands } ) - assert_equal "Unsupported Method: textDocument/didMassage", err.string.chomp + assert_equal "[server] Unsupported Method: textDocument/didMassage", err.string.chomp assert_equal({ id: 18, error: { From fd89000c1d8733570919aa6d1f65bf27ab0678e4 Mon Sep 17 00:00:00 2001 From: Justin Searls Date: Thu, 9 Feb 2023 12:06:39 -0500 Subject: [PATCH 13/13] Revert "switch to Mocktail" Forgot mocktail requires Ruby 3! Wups This reverts commit f094cb2f707e51d0568e0aab46a824938a62d64d. # Conflicts: # Gemfile # Gemfile.lock --- Gemfile | 3 ++- Gemfile.lock | 4 ++-- test/standard/creates_config_store_test.rb | 16 ++++++++-------- test/test_helper.rb | 5 ++--- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Gemfile b/Gemfile index d6295013..af6bf336 100644 --- a/Gemfile +++ b/Gemfile @@ -6,9 +6,10 @@ gem "bundler" gem "minitest", "~> 5.0" gem "pry" gem "rake", "~> 13.0" -gem "mocktail" +gem "gimme" gem "m" + if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.5") gem "simplecov" end diff --git a/Gemfile.lock b/Gemfile.lock index 505cc663..970ce3f9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -12,6 +12,7 @@ GEM ast (2.4.2) coderay (1.1.3) docile (1.4.0) + gimme (0.5.0) json (2.6.3) language_server-protocol (3.17.0.3) m (1.6.1) @@ -19,7 +20,6 @@ GEM rake (>= 0.9.2.2) method_source (1.0.0) minitest (5.17.0) - mocktail (1.2.2) parallel (1.22.1) parser (3.2.0.0) ast (~> 2.4.1) @@ -60,9 +60,9 @@ PLATFORMS DEPENDENCIES bundler + gimme m minitest (~> 5.0) - mocktail pry rake (~> 13.0) simplecov diff --git a/test/standard/creates_config_store_test.rb b/test/standard/creates_config_store_test.rb index f563c370..40406dbe 100644 --- a/test/standard/creates_config_store_test.rb +++ b/test/standard/creates_config_store_test.rb @@ -3,10 +3,10 @@ class Standard::CreatesConfigStore class Test < UnitTest def setup - @assigns_rubocop_yaml = Mocktail.of_next(AssignsRubocopYaml) - @sets_target_ruby_version = Mocktail.of_next(SetsTargetRubyVersion) - @configures_ignored_paths = Mocktail.of_next(ConfiguresIgnoredPaths) - @merges_user_config_extensions = Mocktail.of_next(MergesUserConfigExtensions) + @assigns_rubocop_yaml = gimme_next(AssignsRubocopYaml) + @sets_target_ruby_version = gimme_next(SetsTargetRubyVersion) + @configures_ignored_paths = gimme_next(ConfiguresIgnoredPaths) + @merges_user_config_extensions = gimme_next(MergesUserConfigExtensions) @subject = Standard::CreatesConfigStore.new end @@ -14,13 +14,13 @@ def setup def test_minimal_config standard_config = :some_config options_config = :some_options - stubs { |m| @assigns_rubocop_yaml.call(m.is_a(RuboCop::ConfigStore), standard_config) }.with { options_config } + give(@assigns_rubocop_yaml).call(is_a(RuboCop::ConfigStore), standard_config) { options_config } @subject.call(standard_config) - verify { @sets_target_ruby_version.call(options_config, standard_config) } - verify { @configures_ignored_paths.call(options_config, standard_config) } - verify { @merges_user_config_extensions.call(options_config, standard_config) } + verify(@sets_target_ruby_version).call(options_config, standard_config) + verify(@configures_ignored_paths).call(options_config, standard_config) + verify(@merges_user_config_extensions).call(options_config, standard_config) end end end diff --git a/test/test_helper.rb b/test/test_helper.rb index e855eeff..8de4be9d 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -10,12 +10,11 @@ $LOAD_PATH << "test" require "standard" -require "mocktail" +require "gimme" require "minitest/autorun" require "pry" class UnitTest < Minitest::Test - include Mocktail::DSL make_my_diffs_pretty! def self.path(relative) @@ -23,7 +22,7 @@ def self.path(relative) end def teardown - Mocktail.reset + Gimme.reset end protected