-
Notifications
You must be signed in to change notification settings - Fork 172
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
Integrate builder pattern to refactor hover responses #1317
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,8 @@ | ||
# typed: strict | ||
# frozen_string_literal: true | ||
|
||
require_relative "response_builder" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will be necessary for most of the LSP, so I think we can move this require to |
||
|
||
module RubyLsp | ||
# To register an addon, inherit from this class and implement both `name` and `activate` | ||
# | ||
|
@@ -118,12 +120,13 @@ def create_code_lens_listener(uri, dispatcher); end | |
# Creates a new Hover listener. This method is invoked on every Hover request | ||
sig do | ||
overridable.params( | ||
response_builder: RubyLsp::HoverResponseBuilder, | ||
nesting: T::Array[String], | ||
index: RubyIndexer::Index, | ||
dispatcher: Prism::Dispatcher, | ||
).returns(T.nilable(Listener[T.nilable(Interface::Hover)])) | ||
end | ||
def create_hover_listener(nesting, index, dispatcher); end | ||
def create_hover_listener(response_builder, nesting, index, dispatcher); end | ||
|
||
# Creates a new DocumentSymbol listener. This method is invoked on every DocumentSymbol request | ||
sig do | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,13 @@ | ||
# typed: strict | ||
# frozen_string_literal: true | ||
|
||
require "ruby_lsp/response_builder" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we move to |
||
|
||
module RubyLsp | ||
module Listeners | ||
class Hover < Listener | ||
class Hover | ||
extend T::Sig | ||
extend T::Generic | ||
|
||
ResponseType = type_member { { fixed: T.nilable(Interface::Hover) } } | ||
include Requests::Support::Common | ||
|
||
ALLOWED_TARGETS = T.let( | ||
[ | ||
|
@@ -19,26 +19,23 @@ class Hover < Listener | |
T::Array[T.class_of(Prism::Node)], | ||
) | ||
|
||
sig { override.returns(ResponseType) } | ||
attr_reader :_response | ||
|
||
sig do | ||
params( | ||
response_builder: RubyLsp::HoverResponseBuilder, | ||
uri: URI::Generic, | ||
nesting: T::Array[String], | ||
index: RubyIndexer::Index, | ||
dispatcher: Prism::Dispatcher, | ||
typechecker_enabled: T::Boolean, | ||
).void | ||
end | ||
def initialize(uri, nesting, index, dispatcher, typechecker_enabled) | ||
def initialize(response_builder, uri, nesting, index, dispatcher, typechecker_enabled) # rubocop:disable Metrics/ParameterLists | ||
@response_builder = response_builder | ||
@path = T.let(uri.to_standardized_path, T.nilable(String)) | ||
@nesting = nesting | ||
@index = index | ||
@typechecker_enabled = typechecker_enabled | ||
@_response = T.let(nil, ResponseType) | ||
|
||
super(dispatcher) | ||
dispatcher.register( | ||
self, | ||
:on_constant_read_node_enter, | ||
|
@@ -86,12 +83,7 @@ def on_call_node_enter(node) | |
target_method = @index.resolve_method(message, @nesting.join("::")) | ||
return unless target_method | ||
|
||
location = target_method.location | ||
|
||
@_response = Interface::Hover.new( | ||
range: range_from_location(location), | ||
contents: markdown_from_index_entries(message, target_method), | ||
) | ||
@response_builder << markdown_from_index_entries(message, target_method) | ||
end | ||
|
||
private | ||
|
@@ -106,10 +98,7 @@ def generate_hover(name, location) | |
first_entry = T.must(entries.first) | ||
return if first_entry.visibility == :private && first_entry.name != "#{@nesting.join("::")}::#{name}" | ||
|
||
@_response = Interface::Hover.new( | ||
range: range_from_location(location), | ||
contents: markdown_from_index_entries(name, entries), | ||
) | ||
@response_builder << markdown_from_index_entries(name, entries) | ||
end | ||
|
||
sig { params(node: Prism::CallNode).void } | ||
|
@@ -138,13 +127,7 @@ def generate_gem_hover(node) | |
#{info} | ||
MARKDOWN | ||
|
||
@_response = Interface::Hover.new( | ||
range: range_from_location(node.location), | ||
contents: Interface::MarkupContent.new( | ||
kind: Constant::MarkupKind::MARKDOWN, | ||
value: markdown, | ||
), | ||
) | ||
@response_builder << markdown | ||
rescue Gem::MissingSpecError | ||
# Do nothing if the spec cannot be found | ||
end | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ | |
# frozen_string_literal: true | ||
|
||
require "ruby_lsp/listeners/hover" | ||
require "ruby_lsp/response_builder" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be removed too. |
||
|
||
module RubyLsp | ||
module Requests | ||
|
@@ -52,19 +53,16 @@ def initialize(document, index, position, dispatcher, typechecker_enabled) | |
target = parent | ||
end | ||
|
||
@listeners = T.let([], T::Array[Listener[ResponseType]]) | ||
@listeners = T.let([], T::Array[Listener[HoverResponseBuilder]]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we shouldn't expect the addons' implementation to inherit from Eventually, the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like we can remove this line since we don't use it elsewhere regardless. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed with Stan's comment about the future structure of listeners, but indeed we can just remove this line since it's no longer used. |
||
|
||
# Don't need to instantiate any listeners if there's no target | ||
return unless target | ||
|
||
uri = document.uri | ||
@listeners = T.let( | ||
[Listeners::Hover.new(uri, nesting, index, dispatcher, typechecker_enabled)], | ||
T::Array[Listener[ResponseType]], | ||
) | ||
@response_builder = T.let(HoverResponseBuilder.new, HoverResponseBuilder) | ||
Listeners::Hover.new(@response_builder, uri, nesting, index, dispatcher, typechecker_enabled) | ||
Addon.addons.each do |addon| | ||
addon_listener = addon.create_hover_listener(nesting, index, dispatcher) | ||
@listeners << addon_listener if addon_listener | ||
addon.create_hover_listener(@response_builder, nesting, index, dispatcher) | ||
end | ||
|
||
@target = T.let(target, Prism::Node) | ||
|
@@ -74,18 +72,15 @@ def initialize(document, index, position, dispatcher, typechecker_enabled) | |
sig { override.returns(ResponseType) } | ||
def perform | ||
@dispatcher.dispatch_once(@target) | ||
responses = @listeners.map(&:response).compact | ||
|
||
first_response, *other_responses = responses | ||
|
||
return unless first_response | ||
return if @response_builder.empty? | ||
|
||
# TODO: other_responses should never be nil. Check Sorbet | ||
T.must(other_responses).each do |other_response| | ||
first_response.contents.value << "\n\n" << other_response.contents.value | ||
end | ||
|
||
first_response | ||
Interface::Hover.new( | ||
contents: Interface::MarkupContent.new( | ||
kind: "markdown", | ||
value: @response_builder.build_concatenated_response, | ||
), | ||
) | ||
end | ||
end | ||
end | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -86,7 +86,7 @@ def self_receiver?(node) | |
params( | ||
title: String, | ||
entries: T.any(T::Array[RubyIndexer::Entry], RubyIndexer::Entry), | ||
).returns(Interface::MarkupContent) | ||
).returns(String) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you please double-check if we use this method anywhere else? I kind of expected stuff to break when we changed the type here. If I'm not mistaken we use it for documentation somewhere. Since it now returns a Interface::MarkupContent.new(
kind: "markdown",
value: markdown_from_index_entries(...),
) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great point — see #1328, where I've modified |
||
end | ||
def markdown_from_index_entries(title, entries) | ||
markdown_title = "```ruby\n#{title}\n```" | ||
|
@@ -108,16 +108,13 @@ def markdown_from_index_entries(title, entries) | |
content << "\n\n#{entry.comments.join("\n")}" unless entry.comments.empty? | ||
end | ||
|
||
Interface::MarkupContent.new( | ||
kind: "markdown", | ||
value: <<~MARKDOWN.chomp, | ||
#{markdown_title} | ||
<<~MARKDOWN.chomp | ||
#{markdown_title} | ||
|
||
**Definitions**: #{definitions.join(" | ")} | ||
**Definitions**: #{definitions.join(" | ")} | ||
|
||
#{content} | ||
MARKDOWN | ||
) | ||
#{content} | ||
MARKDOWN | ||
end | ||
end | ||
end | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
# typed: strict | ||
# frozen_string_literal: true | ||
|
||
module RubyLsp | ||
class OperationNotPermitted < StandardError; end | ||
|
||
class ResponseBuilder | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we use the simple There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I agree we should unify the abstract classes. I'll make the change, but to your point, I definitely think we should narrow in to a common builder class before we proceed with the release. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I decided to adopt the builder concept in #1325 (with a slightly different form of structure). But I still think we should not add more common APIs before we've converted most requests. For example, we probably don't want addons to have the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be best to finalize on an approach for the builder in this PR, get this merged, then you can rebase in your PR. I can factor in your approach and modify mine accordingly. Thoughts? Just thinking of the best way to minimize conflicts. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm obviously biased toward my PR 😛 But consider it already adopted #1317 (comment), it may actually be easier if you rebase yours on mine. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense! 😄 |
||
extend T::Sig | ||
extend T::Generic | ||
|
||
Elem = type_member { { upper: Object } } | ||
|
||
sig { void } | ||
def initialize | ||
@response = T.let([], T::Array[Elem]) | ||
end | ||
|
||
sig { params(elem: Elem).void } | ||
def <<(elem) | ||
@response << elem | ||
end | ||
|
||
sig { returns(T.nilable(Elem)) } | ||
def pop | ||
@response.pop | ||
end | ||
|
||
sig { returns(T::Boolean) } | ||
def empty? | ||
@response.empty? | ||
end | ||
end | ||
|
||
class HoverResponseBuilder < ResponseBuilder | ||
extend T::Sig | ||
extend T::Generic | ||
|
||
Elem = type_member { { fixed: String } } | ||
|
||
sig { returns(String) } | ||
def build_concatenated_response | ||
@response.join("\n\n") | ||
end | ||
|
||
sig { override.returns(T.nilable(Elem)) } | ||
def pop | ||
raise OperationNotPermitted, "Cannot pop from a HoverResponseBuilder" | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thought it would be helpful to add a note here since the code below will no longer be accurate for
Hover
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, we'll need to update all of the docs. It might be worth waiting until the refactors are complete and then we update them once.