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

Decouple request and listener logic #1247

Merged
merged 13 commits into from
Jan 11, 2024
Merged

Decouple request and listener logic #1247

merged 13 commits into from
Jan 11, 2024

Conversation

st0012
Copy link
Member

@st0012 st0012 commented Dec 12, 2023

Motivation

Currently, we treat listener-based requests as both a listener and a request. For example, the Hover request handles the instantiation of addon's hover listeners, while being a listener itself to process dispatcher events. This coupling limits the change we can make to improve the interface for addons or more complicated response merging logic.

Implementation

  1. Every request class should have the same root Request class as the parent, which currently only makes sure the response method is called response (instead of run).
  2. A ListenerBasedRequest < Request (name to be improved) class was also created to help listener-based requests define and use listeners. This means:
    • Request classes will handle ALL listener instantiation (built-in and addon's) and merge the responses from them.
    • Listener classes will be solely responsible for handling dispatcher events.
  3. As a result of 2), ExtensibleListener is not needed anymore.
  4. Some request-specific logic in executor.rb are moved to individual request classes as well.

Notes

  • Ideally, listener classes' interface shouldn't be called response anymore. But changing it would break addons and makes this already giant PR even bigger. So I decide to do it later.
  • @Morriar proposed a few interesting ideas to save us from defining ResponseType on both the listener and request classes. But similar to the above, it'll be a huge change on its own and IMO it's better to implement in a follow up PR.

Feedback Needed

  • There are some common pattern for listener classes in the executor.rb, like:
    •   # whole-document-based listener request
        dispatcher = Prism::Dispatcher.new
        document = @store.get(uri)
        request = Requests::DocumentHighlight.new(document, request.dig(:params, :position), dispatcher)
        dispatcher.dispatch(document.tree)
        request.response
    •   # node-based listener request
        dispatcher = Prism::Dispatcher.new
        document = @store.get(uri)
        Requests::Completion.new(
          document,
          @index,
          request.dig(:params, :position),
          document.typechecker_enabled?,
          dispatcher,
        ).response
    Is there a way we can better simplify them (or not)?

Automated Tests

All existing tests should still pass.

Manual Tests

@st0012 st0012 force-pushed the poc-new-listeners branch 4 times, most recently from 733dccd to 01e610b Compare December 18, 2023 18:06
@st0012 st0012 changed the title Remove request logic from CodeLens listener Decouple request and listener logic Dec 18, 2023
@st0012 st0012 force-pushed the poc-new-listeners branch 5 times, most recently from e90f3e2 to 1fee1f8 Compare December 19, 2023 16:54
@st0012 st0012 added the chore Chore task label Dec 19, 2023
@st0012 st0012 self-assigned this Dec 19, 2023
@st0012 st0012 marked this pull request as ready for review December 19, 2023 17:33
@st0012 st0012 requested a review from a team as a code owner December 19, 2023 17:33
@st0012 st0012 force-pushed the poc-new-listeners branch from b7797c9 to 4c73499 Compare January 5, 2024 18:39
lib/ruby_lsp/requests/request.rb Outdated Show resolved Hide resolved
lib/ruby_lsp/requests/request.rb Outdated Show resolved Hide resolved
@st0012 st0012 force-pushed the poc-new-listeners branch 2 times, most recently from 563e29c to 38d6ff2 Compare January 9, 2024 16:52
Copy link
Contributor

@Morriar Morriar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once all requests are proper descendants of Request you should be able to enforce overrides on provider:

class Request
  extend T::Sig
  extend T::Helpers

  abstract!

  class << self
    extend T::Sig

    sig { abstract.returns(T.attached_class) }
    def provider; end
  end
end

class RequestA < Request
  class << self
    sig { override.returns(T.attached_class) }
    def provider
      new
    end
  end
end

class RequestB < Request # Error: Missing definition for abstract method `Request.provider`
end

[sorbet.run]

@st0012 st0012 force-pushed the poc-new-listeners branch from 38d6ff2 to 7883aa2 Compare January 9, 2024 17:17
@st0012
Copy link
Member Author

st0012 commented Jan 9, 2024

@Morriar But provider is not to return the request itself. It's for options like Interface::CodeLensOptions.new(resolve_provider: false) or just a hash. Also, not every request needs it. For example, definition and selection_range don't need to define it.

@st0012 st0012 force-pushed the poc-new-listeners branch 2 times, most recently from e14645b to c84b26c Compare January 9, 2024 17:29
@st0012 st0012 requested a review from vinistock January 9, 2024 20:48
@st0012 st0012 requested a review from Morriar January 9, 2024 20:48
@st0012 st0012 force-pushed the poc-new-listeners branch from c84b26c to 885c4ed Compare January 10, 2024 15:53
lib/ruby_lsp/listeners/code_lens.rb Outdated Show resolved Hide resolved
lib/ruby_lsp/requests/code_lens.rb Outdated Show resolved Hide resolved
# reference
first_entry = T.must(entries.first)
return if first_entry.visibility == :private && first_entry.name != "#{@nesting.join("::")}::#{name}"
# TODO: other_responses should never be nil. Check Sorbet
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this comment be removed? I think the T.must below addresses it, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the comment means that we shouldn't need to wrap T.must around other_responses as it's a splat assignment:

        first_response, *other_responses = responses

It should always be an array regardless of responses's value:

ruby-lsp(main):008> x, *y = a; y
=> []
ruby-lsp(main):009> a = nil
=> nil
ruby-lsp(main):010> x, *y = a; y
=> []
ruby-lsp(main):011> 

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, Sorbet is incorrect in this case (unless I'm missing something). I tried a bunch of variations and in the a, *b = pattern, b is always an array and never nilable.

Can we please check if there's an issue for that and, if not, create one? Feels like something that shouldn't be too hard to address (famous last words).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes there's an 4-year old issue for it: sorbet/sorbet#2020
To be fair, it was fixed but then the fix was reverted.

test/requests/document_symbol_expectations_test.rb Outdated Show resolved Hide resolved
@st0012 st0012 force-pushed the poc-new-listeners branch from 885c4ed to 5193d9f Compare January 10, 2024 21:33
@st0012 st0012 requested a review from vinistock January 10, 2024 21:36
Copy link
Member

@vinistock vinistock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caught one bug while tophatting

false
sig { override.returns(ResponseType) }
def response
@dispatcher.dispatch_once(@target)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is failing because @dispatcher is nil on certain conditions due to the early returns in initialize.

I think we should move most of the code in the initialize method to response and ensure that the key instance variables are always set.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer not moving most of them into response as it means we'd need to initialise listeners there too, which doesn't align with other requests.
I've rearranged the ordering in initialize a bit instead so all the ivars used in response should always be defined.

@st0012 st0012 force-pushed the poc-new-listeners branch from 5193d9f to b4667df Compare January 11, 2024 15:34
@st0012 st0012 requested a review from vinistock January 11, 2024 15:35
@st0012 st0012 merged commit 31aa8ef into main Jan 11, 2024
30 of 36 checks passed
@st0012 st0012 deleted the poc-new-listeners branch January 11, 2024 16:43
@st0012 st0012 removed the chore Chore task label Jan 11, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants