Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Ruby LSP as a built-in add-on #630

Merged
merged 15 commits into from
Jun 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .standard.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ruby_version: 2.6
ruby_version: 3.0
ignore:
- tmp/**/*
- test/fixture/**/*
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ gem "bundler"
gem "minitest", "~> 5.0"
gem "rake", "~> 13.0"
gem "m"
gem "mutex_m"
gem "ruby-lsp"

# You may want to run these off path locally:
# gem "lint_roller", path: "../lint_roller"
Expand Down
9 changes: 9 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ GEM
rake (>= 0.9.2.2)
method_source (1.0.0)
minitest (5.20.0)
mutex_m (0.2.0)
parallel (1.23.0)
parser (3.3.0.5)
ast (~> 2.4.1)
racc
prism (0.27.0)
racc (1.7.1)
rainbow (3.1.1)
rake (13.0.6)
Expand All @@ -46,13 +48,18 @@ GEM
rubocop-performance (1.21.0)
rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
ruby-lsp (0.16.6)
language_server-protocol (~> 3.17.0)
prism (>= 0.23.0, < 0.28)
sorbet-runtime (>= 0.5.10782)
ruby-progressbar (1.13.0)
simplecov (0.22.0)
docile (~> 1.1)
simplecov-html (~> 0.11)
simplecov_json_formatter (~> 0.1)
simplecov-html (0.12.3)
simplecov_json_formatter (0.1.4)
sorbet-runtime (0.5.11385)
standard-custom (1.0.2)
lint_roller (~> 1.0)
rubocop (~> 1.50)
Expand All @@ -69,7 +76,9 @@ DEPENDENCIES
bundler
m
minitest (~> 5.0)
mutex_m
rake (~> 13.0)
ruby-lsp
simplecov
standard!

Expand Down
60 changes: 60 additions & 0 deletions lib/ruby_lsp/standard/addon.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
require "standard"
require_relative "wraps_built_in_lsp_standardizer"

module RubyLsp
module Standard
class Addon < ::RubyLsp::Addon
def initializer
@wraps_built_in_lsp_standardizer = nil
end

def name
"Standard Ruby"
end

def activate(global_state, message_queue)
warn "Activating Standard Ruby LSP addon v#{::Standard::VERSION}"
@wraps_built_in_lsp_standardizer = WrapsBuiltinLspStandardizer.new
global_state.register_formatter("standard", @wraps_built_in_lsp_standardizer)
register_additional_file_watchers(global_state, message_queue)
warn "Initialized Standard Ruby LSP addon #{::Standard::VERSION}"
end

def deactivate
@wraps_built_in_lsp_standardizer = nil
end

def register_additional_file_watchers(global_state, message_queue)
return unless global_state.supports_watching_files

message_queue << Request.new(
id: "standard-file-watcher",
method: "client/registerCapability",
params: Interface::RegistrationParams.new(
registrations: [
Interface::Registration.new(
id: "workspace/didChangeWatchedFilesStandard",
method: "workspace/didChangeWatchedFiles",
register_options: Interface::DidChangeWatchedFilesRegistrationOptions.new(
watchers: [
Interface::FileSystemWatcher.new(
glob_pattern: "**/.standard.yml",
kind: Constant::WatchKind::CREATE | Constant::WatchKind::CHANGE | Constant::WatchKind::DELETE
)
]
)
)
]
)
)
end

def workspace_did_change_watched_files(changes)
if changes.any? { |change| change[:uri].end_with?(".standard.yml") }
@wraps_built_in_lsp_standardizer.init!
warn "Re-initialized Standard Ruby LSP addon #{::Standard::VERSION} due to .standard.yml file change"
end
end
end
end
end
98 changes: 98 additions & 0 deletions lib/ruby_lsp/standard/wraps_built_in_lsp_standardizer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
module RubyLsp
module Standard
class WrapsBuiltinLspStandardizer
include RubyLsp::Requests::Support::Formatter
def initialize
init!
end

def init!
@config = ::Standard::BuildsConfig.new.call([])
@standardizer = ::Standard::Lsp::Standardizer.new(
@config,
::Standard::Lsp::Logger.new
searls marked this conversation as resolved.
Show resolved Hide resolved
)
@rubocop_config = @config.rubocop_config_store.for_pwd
@cop_registry = RuboCop::Cop::Registry.global.to_h
end

def run_formatting(uri, document)
@standardizer.format(uri_to_path(uri), document.source)
searls marked this conversation as resolved.
Show resolved Hide resolved
end

def run_diagnostic(uri, document)
offenses = @standardizer.offenses(uri_to_path(uri), document.source)
searls marked this conversation as resolved.
Show resolved Hide resolved

offenses.map { |o|
cop_name = o[:cop_name]

msg = o[:message].delete_prefix(cop_name)
loc = o[:location]

severity = case o[:severity]
when "error", "fatal"
RubyLsp::Constant::DiagnosticSeverity::ERROR
when "warning"
RubyLsp::Constant::DiagnosticSeverity::WARNING
when "convention"
RubyLsp::Constant::DiagnosticSeverity::INFORMATION
when "refactor", "info"
RubyLsp::Constant::DiagnosticSeverity::HINT
else # the above cases fully cover what RuboCop sends at this time
logger.puts "Unknown severity: #{severity.inspect}"
searls marked this conversation as resolved.
Show resolved Hide resolved
RubyLsp::Constant::DiagnosticSeverity::HINT
end

RubyLsp::Interface::Diagnostic.new(
code: cop_name,
code_description: code_description(cop_name),
message: msg,
source: "Standard Ruby",
severity: severity,
range: RubyLsp::Interface::Range.new(
start: RubyLsp::Interface::Position.new(line: loc[:start_line] - 1, character: loc[:start_column] - 1),
end: RubyLsp::Interface::Position.new(line: loc[:last_line] - 1, character: loc[:last_column])
)
# TODO: We need to do something like to support quickfixes thru code actions
# See: https://github.com/Shopify/ruby-lsp/blob/4c1906172add4d5c39c35d3396aa29c768bfb898/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb#L62
# data: {
# correctable: correctable?(offense),
# code_actions: to_lsp_code_actions
# }
#
# Right now, our offenses are all just JSON parsed from stdout shelling to RuboCop, so
# it seems we don't have the corrector available to us.
#
# Lifted from:
# https://github.com/Shopify/ruby-lsp/blob/8d4c17efce4e8ecc8e7c557ab2981db6b22c0b6d/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb#L201
# def correctable?(offense)
# !offense.corrector.nil?
# end
)
}
end

private

# duplicated from: lib/standard/lsp/routes.rb
# modified to incorporate Ruby LSP's to_standardized_path method
def uri_to_path(uri)
if uri.respond_to?(:to_standardized_path) && !(standardized_path = uri.to_standardized_path).nil?
standardized_path
else
uri.to_s.sub(%r{^file://}, "")
end
end

# lifted from:
# https://github.com/Shopify/ruby-lsp/blob/4c1906172add4d5c39c35d3396aa29c768bfb898/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb#L84
def code_description(cop_name)
searls marked this conversation as resolved.
Show resolved Hide resolved
if (cop_class = @cop_registry[cop_name]&.first)
if (doc_url = cop_class.documentation_url(@rubocop_config))
Interface::CodeDescription.new(href: doc_url)
end
end
end
end
end
end
2 changes: 1 addition & 1 deletion lib/standard/plugin/merges_plugins_into_rubocop_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ def blank_rubocop_config(example_config)
end

def except(hash_or_config, keys)
hash_or_config.to_h.reject { |key, _| keys.include?(key) }.to_h
hash_or_config.to_h.except(*keys).to_h
end

# Always deletes nil entries, always overwrites arrays
Expand Down
Empty file.
2 changes: 2 additions & 0 deletions test/fixture/ruby_lsp/simple.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
s = 'hi'
puts s
125 changes: 125 additions & 0 deletions test/ruby_lsp_addon_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# All of these requires were needed because `ruby_lsp/internal` mutates rubocop
# in a way that breaks test/standard/cli_test.rb
require "sorbet-runtime"
require "language_server-protocol"
require "ruby_lsp/base_server"
require "ruby_lsp/server"
require "ruby_lsp/requests"
require "ruby_lsp/addon"
require "ruby_lsp/utils"
require "ruby_lsp/store"
require "ruby_lsp/document"
require "ruby_lsp/global_state"
require "core_ext/uri"
require "ruby_indexer/ruby_indexer"
require "ruby_lsp/ruby_document"
require "prism"
require "ruby_lsp/standard/addon"

require_relative "test_helper"

class RubyLspAddonTest < UnitTest
def setup
@addon = RubyLsp::Standard::Addon.new
super
end

def test_name
assert_equal "Standard Ruby", @addon.name
end

def test_diagnostic
source = <<~RUBY
s = 'hello'
puts s
RUBY
with_server(source, "simple.rb") do |server, uri|
server.process_message(
id: 2,
method: "textDocument/diagnostic",
params: {
textDocument: {
uri: uri
}
}
)

result = server.pop_response

assert_instance_of(RubyLsp::Result, result)
assert_equal "full", result.response.kind
assert_equal 1, result.response.items.size
item = result.response.items.first
assert_equal({line: 0, character: 4}, item.range.start.to_hash)
assert_equal({line: 0, character: 11}, item.range.end.to_hash)
assert_equal RubyLsp::Constant::DiagnosticSeverity::INFORMATION, item.severity
assert_equal "Style/StringLiterals", item.code
assert_equal "https://docs.rubocop.org/rubocop/cops_style.html#stylestringliterals", item.code_description.href
assert_equal "Standard Ruby", item.source
assert_equal "Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.", item.message
end
end

def test_format
source = <<~RUBY
s = 'hello'
puts s
RUBY
with_server(source, "simple.rb") do |server, uri|
server.process_message(
id: 2,
method: "textDocument/formatting",
params: {textDocument: {uri: uri}, position: {line: 0, character: 0}}
)

result = server.pop_response

assert_instance_of(RubyLsp::Result, result)
assert 1, result.response.size
assert_equal <<~RUBY, result.response.first.new_text
s = "hello"
puts s
RUBY
end
end

private

# Lifted from here, because we need to override the formatter to "standard" in the test helper:
# https://github.com/Shopify/ruby-lsp/blob/4c1906172add4d5c39c35d3396aa29c768bfb898/lib/ruby_lsp/test_helper.rb#L20
def with_server(source = nil, path = "fake.rb", pwd: "test/fixture/ruby_lsp", stub_no_typechecker: false, load_addons: true,
&block)
Dir.chdir pwd do
server = RubyLsp::Server.new(test_mode: true)
uri = Kernel.URI(File.join(server.global_state.workspace_path, path))
server.global_state.formatter = "standard"
server.global_state.instance_variable_set(:@linters, ["standard"])
server.global_state.stubs(:typechecker).returns(false) if stub_no_typechecker

if source
server.process_message({
method: "textDocument/didOpen",
params: {
textDocument: {
uri: uri,
text: source,
version: 1
}
}
})
end

server.global_state.index.index_single(
RubyIndexer::IndexablePath.new(nil, uri.to_standardized_path),
source
)
server.load_addons if load_addons
block.call(server, uri)
end
ensure
if load_addons
RubyLsp::Addon.addons.each(&:deactivate)
RubyLsp::Addon.addons.clear
end
end
end
Loading