Skip to content

Commit

Permalink
Handle granular requests configuration (#1234)
Browse files Browse the repository at this point in the history
* feat: handle granular configuration for inlayHint

* feat: handle granular configuration for codeLens

* clean: use RequestConfig class and switch off by default hints

* clean: remove configuration from server response to client
  • Loading branch information
snutij authored Dec 5, 2023
1 parent 68543e3 commit 005de90
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 12 deletions.
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

0 comments on commit 005de90

Please sign in to comment.