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

Handle granular requests configuration #1234

Merged
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
11 changes: 9 additions & 2 deletions lib/ruby_lsp/executor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ def run(request)
folding_range = Requests::FoldingRanges.new(document.parse_result.comments, dispatcher)
document_symbol = Requests::DocumentSymbol.new(dispatcher)
document_link = Requests::DocumentLink.new(uri, document.comments, dispatcher)
code_lens = Requests::CodeLens.new(uri, dispatcher)
lenses_configuration = T.must(@store.features_configuration.dig(:codeLens))
code_lens = Requests::CodeLens.new(uri, lenses_configuration, dispatcher)

semantic_highlighting = Requests::SemanticHighlighting.new(dispatcher)
dispatcher.dispatch(document.tree)
Expand Down Expand Up @@ -392,7 +393,8 @@ def inlay_hint(uri, range)
end_line = range.dig(:end, :line)

dispatcher = Prism::Dispatcher.new
listener = Requests::InlayHints.new(start_line..end_line, dispatcher)
hints_configurations = T.must(@store.features_configuration.dig(:inlayHint))
listener = Requests::InlayHints.new(start_line..end_line, hints_configurations, dispatcher)
dispatcher.visit(document.tree)
listener.response
end
Expand Down Expand Up @@ -602,6 +604,11 @@ def initialize_request(options)
configured_features = options.dig(:initializationOptions, :enabledFeatures)
@store.experimental_features = options.dig(:initializationOptions, :experimentalFeaturesEnabled) || false

configured_hints = options.dig(:initializationOptions, :featuresConfiguration, :inlayHint)
configured_lenses = options.dig(:initializationOptions, :featuresConfiguration, :codeLens)
T.must(@store.features_configuration.dig(:inlayHint)).configuration.merge!(configured_hints) if configured_hints
T.must(@store.features_configuration.dig(:codeLens)).configuration.merge!(configured_lenses) if configured_lenses

enabled_features = case configured_features
when Array
# If the configuration is using an array, then absent features are disabled and present ones are enabled. That's
Expand Down
17 changes: 15 additions & 2 deletions lib/ruby_lsp/requests/code_lens.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ module Requests
# [code lens](https://microsoft.github.io/language-server-protocol/specification#textDocument_codeLens)
# request informs the editor of runnable commands such as tests
#
# # Configuration
#
# To disable gem code lenses, set `rubyLsp.featuresConfiguration.codeLens.gemfileLinks` to `false`.
#
# # Example
#
# ```ruby
Expand Down Expand Up @@ -47,8 +51,14 @@ class CodeLens < ExtensibleListener
sig { override.returns(ResponseType) }
attr_reader :_response

sig { params(uri: URI::Generic, dispatcher: Prism::Dispatcher).void }
def initialize(uri, dispatcher)
sig do
params(
uri: URI::Generic,
lenses_configuration: RequestConfig,
dispatcher: Prism::Dispatcher,
).void
end
def initialize(uri, lenses_configuration, dispatcher)
@uri = T.let(uri, URI::Generic)
@_response = T.let([], ResponseType)
@path = T.let(uri.to_standardized_path, T.nilable(String))
Expand All @@ -57,6 +67,7 @@ def initialize(uri, dispatcher)
@class_stack = T.let([], T::Array[String])
@group_id = T.let(1, Integer)
@group_id_stack = T.let([], T::Array[Integer])
@lenses_configuration = lenses_configuration

super(dispatcher)

Expand Down Expand Up @@ -134,6 +145,8 @@ def on_call_node_enter(node)
end

if @path&.include?(GEMFILE_NAME) && name == :gem && arguments
return unless @lenses_configuration.enabled?(:gemfileLinks)

first_argument = arguments.arguments.first
return unless first_argument.is_a?(Prism::StringNode)

Expand Down
21 changes: 19 additions & 2 deletions lib/ruby_lsp/requests/inlay_hints.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ module Requests
# are labels added directly in the code that explicitly show the user something that might
# otherwise just be implied.
#
# # Configuration
#
# To enable rescue hints, set `rubyLsp.featuresConfiguration.inlayHint.implicitRescue` to `true`.
#
# To enable hash value hints, set `rubyLsp.featuresConfiguration.inlayHint.implicitHashValue` to `true`.
#
# To enable all hints, set `rubyLsp.featuresConfiguration.inlayHint.enableAll` to `true`.
#
# # Example
#
# ```ruby
Expand Down Expand Up @@ -39,18 +47,26 @@ class InlayHints < Listener
sig { override.returns(ResponseType) }
attr_reader :_response

sig { params(range: T::Range[Integer], dispatcher: Prism::Dispatcher).void }
def initialize(range, dispatcher)
sig do
params(
range: T::Range[Integer],
hints_configuration: RequestConfig,
dispatcher: Prism::Dispatcher,
).void
end
def initialize(range, hints_configuration, dispatcher)
super(dispatcher)

@_response = T.let([], ResponseType)
@range = range
@hints_configuration = hints_configuration

dispatcher.register(self, :on_rescue_node_enter, :on_implicit_node_enter)
end

sig { params(node: Prism::RescueNode).void }
def on_rescue_node_enter(node)
return unless @hints_configuration.enabled?(:implicitRescue)
return unless node.exceptions.empty?

loc = node.location
Expand All @@ -66,6 +82,7 @@ def on_rescue_node_enter(node)

sig { params(node: Prism::ImplicitNode).void }
def on_implicit_node_enter(node)
return unless @hints_configuration.enabled?(:implicitHashValue)
return unless visible?(node, @range)

node_value = node.value
Expand Down
17 changes: 17 additions & 0 deletions lib/ruby_lsp/store.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ class Store
sig { returns(URI::Generic) }
attr_accessor :workspace_uri

sig { returns(T::Hash[Symbol, RequestConfig]) }
attr_accessor :features_configuration

sig { void }
def initialize
@state = T.let({}, T::Hash[String, Document])
Expand All @@ -28,6 +31,20 @@ def initialize
@supports_progress = T.let(true, T::Boolean)
@experimental_features = T.let(false, T::Boolean)
@workspace_uri = T.let(URI::Generic.from_path(path: Dir.pwd), URI::Generic)
@features_configuration = T.let(
{
codeLens: RequestConfig.new({
enableAll: false,
gemfileLinks: true,
}),
inlayHint: RequestConfig.new({
enableAll: false,
implicitRescue: false,
implicitHashValue: false,
}),
},
T::Hash[Symbol, RequestConfig],
)
end

sig { params(uri: URI::Generic).returns(Document) }
Expand Down
18 changes: 18 additions & 0 deletions lib/ruby_lsp/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,22 @@ def cancel
@cancelled = true
end
end

# A request configuration, to turn on/off features
class RequestConfig
extend T::Sig

sig { returns(T::Hash[Symbol, T::Boolean]) }
attr_accessor :configuration

sig { params(configuration: T::Hash[Symbol, T::Boolean]).void }
def initialize(configuration)
@configuration = configuration
end

sig { params(feature: Symbol).returns(T.nilable(T::Boolean)) }
def enabled?(feature)
@configuration[:enableAll] || @configuration[feature]
end
end
end
70 changes: 70 additions & 0 deletions test/executor_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,76 @@ def test_did_close_clears_diagnostics
@store.delete(uri)
end

def test_initialize_features_with_default_configuration
RubyLsp::Executor.new(@store, @message_queue)
.execute(method: "initialize", params: { initializationOptions: {} })

assert(@store.features_configuration.dig(:codeLens).enabled?(:gemfileLinks))
refute(@store.features_configuration.dig(:inlayHint).enabled?(:implicitRescue))
refute(@store.features_configuration.dig(:inlayHint).enabled?(:implicitHashValue))
end

def test_initialize_features_with_provided_configuration
RubyLsp::Executor.new(@store, @message_queue)
.execute(method: "initialize", params: {
initializationOptions: {
featuresConfiguration: {
codeLens: {
gemfileLinks: false,
},
inlayHint: {
implicitRescue: true,
implicitHashValue: true,
},
},
},
})

refute(@store.features_configuration.dig(:codeLens).enabled?(:gemfileLinks))
assert(@store.features_configuration.dig(:inlayHint).enabled?(:implicitRescue))
assert(@store.features_configuration.dig(:inlayHint).enabled?(:implicitHashValue))
end

def test_initialize_features_with_partially_provided_configuration
RubyLsp::Executor.new(@store, @message_queue)
.execute(method: "initialize", params: {
initializationOptions: {
featuresConfiguration: {
codeLens: {
gemfileLinks: false,
},
inlayHint: {
implicitHashValue: true,
},
},
},
})

refute(@store.features_configuration.dig(:codeLens).enabled?(:gemfileLinks))
refute(@store.features_configuration.dig(:inlayHint).enabled?(:implicitRescue))
assert(@store.features_configuration.dig(:inlayHint).enabled?(:implicitHashValue))
end

def test_initialize_features_with_enable_all_configuration
RubyLsp::Executor.new(@store, @message_queue)
.execute(method: "initialize", params: {
initializationOptions: {
featuresConfiguration: {
codeLens: {
enableAll: true,
},
inlayHint: {
enableAll: true,
},
},
},
})

assert(@store.features_configuration.dig(:codeLens).enabled?(:gemfileLinks))
assert(@store.features_configuration.dig(:inlayHint).enabled?(:implicitRescue))
assert(@store.features_configuration.dig(:inlayHint).enabled?(:implicitHashValue))
end

def test_detects_rubocop_if_direct_dependency
stub_dependencies(rubocop: true, syntax_tree: false)
RubyLsp::Executor.new(@store, @message_queue)
Expand Down
28 changes: 23 additions & 5 deletions test/requests/code_lens_expectations_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def run_expectations(source)

dispatcher = Prism::Dispatcher.new
stub_test_library("minitest")
listener = RubyLsp::Requests::CodeLens.new(uri, dispatcher)
listener = RubyLsp::Requests::CodeLens.new(uri, default_lenses_configuration, dispatcher)
dispatcher.dispatch(document.tree)
listener.response
end
Expand All @@ -30,7 +30,7 @@ def test_bar; end
document = RubyLsp::RubyDocument.new(source: source, version: 1, uri: uri)

dispatcher = Prism::Dispatcher.new
listener = RubyLsp::Requests::CodeLens.new(uri, dispatcher)
listener = RubyLsp::Requests::CodeLens.new(uri, default_lenses_configuration, dispatcher)
dispatcher.dispatch(document.tree)
response = listener.response

Expand All @@ -57,7 +57,7 @@ def test_bar; end

dispatcher = Prism::Dispatcher.new
stub_test_library("unknown")
listener = RubyLsp::Requests::CodeLens.new(uri, dispatcher)
listener = RubyLsp::Requests::CodeLens.new(uri, default_lenses_configuration, dispatcher)
dispatcher.dispatch(document.tree)
response = listener.response

Expand All @@ -76,7 +76,7 @@ def test_bar; end

dispatcher = Prism::Dispatcher.new
stub_test_library("rspec")
listener = RubyLsp::Requests::CodeLens.new(uri, dispatcher)
listener = RubyLsp::Requests::CodeLens.new(uri, default_lenses_configuration, dispatcher)
dispatcher.dispatch(document.tree)
response = listener.response

Expand All @@ -95,13 +95,27 @@ def test_bar; end

dispatcher = Prism::Dispatcher.new
stub_test_library("minitest")
listener = RubyLsp::Requests::CodeLens.new(uri, dispatcher)
listener = RubyLsp::Requests::CodeLens.new(uri, default_lenses_configuration, dispatcher)
dispatcher.dispatch(document.tree)
response = listener.response

assert_empty(response)
end

def test_skip_gemfile_links
uri = URI("file:///Gemfile")
document = RubyLsp::RubyDocument.new(uri: uri, source: <<~RUBY, version: 1)
gem 'minitest'
RUBY

dispatcher = Prism::Dispatcher.new
lenses_configuration = RubyLsp::RequestConfig.new({ gemfileLinks: false })
listener = RubyLsp::Requests::CodeLens.new(uri, lenses_configuration, dispatcher)
dispatcher.dispatch(document.tree)
response = listener.response
assert_empty(response)
end

def test_code_lens_addons
source = <<~RUBY
class Test < Minitest::Test; end
Expand All @@ -123,6 +137,10 @@ class Test < Minitest::Test; end

private

def default_lenses_configuration
RubyLsp::RequestConfig.new({ gemfileLinks: true })
end

def create_code_lens_addon
Class.new(RubyLsp::Addon) do
def activate(message_queue); end
Expand Down
31 changes: 30 additions & 1 deletion test/requests/inlay_hints_expectations_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,41 @@ def run_expectations(source)
document = RubyLsp::RubyDocument.new(source: source, version: 1, uri: uri)

dispatcher = Prism::Dispatcher.new
listener = RubyLsp::Requests::InlayHints.new(params.first, dispatcher)
hints_configuration = RubyLsp::RequestConfig.new({ implicitRescue: true, implicitHashValue: true })
listener = RubyLsp::Requests::InlayHints.new(params.first, hints_configuration, dispatcher)
dispatcher.dispatch(document.tree)
listener.response
end

def default_args
[0..20]
end

def test_skip_implicit_hash_value
uri = URI("file://foo.rb")
document = RubyLsp::RubyDocument.new(uri: uri, source: <<~RUBY, version: 1)
{bar:, baz:}
RUBY

dispatcher = Prism::Dispatcher.new
hints_configuration = RubyLsp::RequestConfig.new({ implicitRescue: true, implicitHashValue: false })
listener = RubyLsp::Requests::InlayHints.new(default_args.first, hints_configuration, dispatcher)
dispatcher.dispatch(document.tree)
assert_empty(listener.response)
end

def test_skip_implicit_rescue
uri = URI("file://foo.rb")
document = RubyLsp::RubyDocument.new(uri: uri, source: <<~RUBY, version: 1)
begin
rescue
end
RUBY

dispatcher = Prism::Dispatcher.new
hints_configuration = RubyLsp::RequestConfig.new({ implicitRescue: false, implicitHashValue: true })
listener = RubyLsp::Requests::InlayHints.new(default_args.first, hints_configuration, dispatcher)
dispatcher.dispatch(document.tree)
assert_empty(listener.response)
end
end
Loading