Skip to content

Integrate document symbol for callbacks #280

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

Merged
merged 2 commits into from
Mar 7, 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
169 changes: 164 additions & 5 deletions lib/ruby_lsp/ruby_lsp_rails/document_symbol.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,63 @@ class DocumentSymbol
include Requests::Support::Common
include ActiveSupportTestCaseHelper

MODEL_CALLBACKS = T.let(
[
"before_validation",
"after_validation",
"before_save",
"around_save",
"after_save",
"before_create",
"around_create",
"after_create",
"after_commit",
"after_rollback",
"before_update",
"around_update",
"after_update",
"before_destroy",
"around_destroy",
"after_destroy",
"after_initialize",
"after_find",
"after_touch",
].freeze,
T::Array[String],
)

CONTROLLER_CALLBACKS = T.let(
[
"after_action",
"append_after_action",
"append_around_action",
"append_before_action",
"around_action",
"before_action",
"prepend_after_action",
"prepend_around_action",
"prepend_before_action",
"skip_after_action",
"skip_around_action",
"skip_before_action",
].freeze,
T::Array[String],
)

JOB_CALLBACKS = T.let(
[
"after_enqueue",
"after_perform",
"around_enqueue",
"around_perform",
"before_enqueue",
"before_perform",
].freeze,
T::Array[String],
)

CALLBACKS = T.let((MODEL_CALLBACKS + CONTROLLER_CALLBACKS + JOB_CALLBACKS).freeze, T::Array[String])

sig do
params(
response_builder: ResponseBuilders::DocumentSymbol,
Expand All @@ -28,13 +85,115 @@ def initialize(response_builder, dispatcher)
def on_call_node_enter(node)
content = extract_test_case_name(node)

return unless content
if content
append_document_symbol(
name: content,
selection_range: range_from_node(node),
range: range_from_node(node),
)
end

extract_callbacks(node)
end

private

sig { params(node: Prism::CallNode).void }
def extract_callbacks(node)
receiver = node.receiver
return if receiver && !receiver.is_a?(Prism::SelfNode)

message_value = node.message

return unless CALLBACKS.include?(message_value)

block = node.block

if block
append_document_symbol(
name: "#{message_value}(<anonymous>)",
range: range_from_location(node.location),
selection_range: range_from_location(block.location),
)
return
end

arguments = node.arguments&.arguments
return unless arguments&.any?

arguments.each do |argument|
case argument
when Prism::SymbolNode
name = argument.value
next unless name

append_document_symbol(
name: "#{message_value}(#{name})",
range: range_from_location(argument.location),
selection_range: range_from_location(T.must(argument.value_loc)),
)
when Prism::StringNode
name = argument.content
next if name.empty?

append_document_symbol(
name: "#{message_value}(#{name})",
range: range_from_location(argument.location),
selection_range: range_from_location(argument.content_loc),
)
when Prism::LambdaNode
append_document_symbol(
name: "#{message_value}(<anonymous>)",
range: range_from_location(node.location),
selection_range: range_from_location(argument.location),
)
when Prism::CallNode
arg_receiver = argument.receiver

name = arg_receiver.name if arg_receiver.is_a?(Prism::ConstantReadNode)
name = arg_receiver.full_name if arg_receiver.is_a?(Prism::ConstantPathNode)
next unless name

append_document_symbol(
name: "#{message_value}(#{name})",
range: range_from_location(argument.location),
selection_range: range_from_location(argument.location),
)
when Prism::ConstantReadNode
name = argument.name
next if name.empty?

append_document_symbol(
name: "#{message_value}(#{name})",
range: range_from_location(argument.location),
selection_range: range_from_location(argument.location),
)
when Prism::ConstantPathNode
name = argument.full_name
next if name.empty?

append_document_symbol(
name: "#{message_value}(#{name})",
range: range_from_location(argument.location),
selection_range: range_from_location(argument.location),
)
end
end
end

sig do
params(
name: String,
range: RubyLsp::Interface::Range,
selection_range: RubyLsp::Interface::Range,
).void
end
def append_document_symbol(name:, range:, selection_range:)
@response_builder.last.children << RubyLsp::Interface::DocumentSymbol.new(
name: content,
kind: LanguageServer::Protocol::Constant::SymbolKind::METHOD,
selection_range: range_from_node(node),
range: range_from_node(node),
name: name,
kind: RubyLsp::Constant::SymbolKind::METHOD,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This affects the icon of the symbol.

I chose the same approach as the base Ruby LSP. The options are listed in this link — I don't think it's optimal to use any other config.

range: range,
selection_range: selection_range,
)
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ def extract_test_case_name(node)
message_value = node.message
return unless message_value == "test" || message_value == "it"

return unless node.arguments

Comment on lines -14 to -15
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is not in the direct scope of this PR, but I've kept it to a separate commit; it is used by the DocumentSymbol class. The line below already accomplishments what this does.

arguments = node.arguments&.arguments
return unless arguments&.any?

Expand Down
1 change: 1 addition & 0 deletions test/dummy/app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
# frozen_string_literal: true

class User < ApplicationRecord
before_create :foo_arg, -> () {}
end
124 changes: 124 additions & 0 deletions test/ruby_lsp_rails/document_symbol_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,130 @@ class NestedTest < ActiveSupport::TestCase
assert_equal("back to the same level", response[0].children[2].name)
end

test "correctly handles model callbacks with multiple Prism::StringNode arguments" do
response = generate_document_symbols_for_source(<<~RUBY)
class FooModel < ApplicationRecord
before_save "foo_method", "bar_method", on: :update
end
RUBY

assert_equal(1, response.size)
assert_equal("FooModel", response[0].name)
assert_equal(2, response[0].children.size)
assert_equal("before_save(foo_method)", response[0].children[0].name)
assert_equal("before_save(bar_method)", response[0].children[1].name)
end

test "correctly handles controller callback with block" do
response = generate_document_symbols_for_source(<<~RUBY)
class FooController < ApplicationController
before_action do
# block body
end
end
RUBY

assert_equal(1, response.size)
assert_equal("FooController", response[0].name)
assert_equal(1, response[0].children.size)
assert_equal("before_action(<anonymous>)", response[0].children[0].name)
end

test "correctly handles job callback with Prism::SymbolNode argument" do
response = generate_document_symbols_for_source(<<~RUBY)
class FooJob < ApplicationJob
before_perform :foo_method
end
RUBY

assert_equal(1, response.size)
assert_equal("FooJob", response[0].name)
assert_equal(1, response[0].children.size)
assert_equal("before_perform(foo_method)", response[0].children[0].name)
end

test "correctly handles model callback with Prism::LambdaNode argument" do
response = generate_document_symbols_for_source(<<~RUBY)
class FooModel < ApplicationRecord
before_save -> () {}
end
RUBY

assert_equal(1, response.size)
assert_equal("FooModel", response[0].name)
assert_equal(1, response[0].children.size)
assert_equal("before_save(<anonymous>)", response[0].children[0].name)
end

test "correctly handles job callbacks with Prism::CallNode argument" do
response = generate_document_symbols_for_source(<<~RUBY)
class FooJob < ApplicationJob
before_perform FooClass.new(foo_arg)
end
RUBY

assert_equal(1, response.size)
assert_equal("FooJob", response[0].name)
assert_equal(1, response[0].children.size)
assert_equal("before_perform(FooClass)", response[0].children[0].name)
end

test "correctly handles controller callbacks with Prism::ConstantReadNode argument" do
response = generate_document_symbols_for_source(<<~RUBY)
class FooController < ApplicationController
before_action FooClass
end
RUBY

assert_equal(1, response.size)
assert_equal("FooController", response[0].name)
assert_equal(1, response[0].children.size)
assert_equal("before_action(FooClass)", response[0].children[0].name)
end

test "correctly handles model callbacks with Prism::ConstantPathNode argument" do
response = generate_document_symbols_for_source(<<~RUBY)
class FooModel < ApplicationRecord
before_save Foo::BarClass
end
RUBY

assert_equal(1, response.size)
assert_equal("FooModel", response[0].name)
assert_equal(1, response[0].children.size)
assert_equal("before_save(Foo::BarClass)", response[0].children[0].name)
end

test "correctly handles job callbacks with all argument types" do
response = generate_document_symbols_for_source(<<~RUBY)
class FooJob < ApplicationJob
before_perform "foo_arg", :bar_arg, -> () {}, Foo::BazClass.new("blah"), FooClass, Foo::BarClass
end
RUBY

assert_equal(1, response.size)
assert_equal("FooJob", response[0].name)
assert_equal(6, response[0].children.size)
assert_equal("before_perform(foo_arg)", response[0].children[0].name)
assert_equal("before_perform(bar_arg)", response[0].children[1].name)
assert_equal("before_perform(<anonymous>)", response[0].children[2].name)
assert_equal("before_perform(Foo::BazClass)", response[0].children[3].name)
assert_equal("before_perform(FooClass)", response[0].children[4].name)
assert_equal("before_perform(Foo::BarClass)", response[0].children[5].name)
end

test "ignore unrecognized callback" do
response = generate_document_symbols_for_source(<<~RUBY)
class FooJob < ApplicationJob
unrecognized_callback :foo_method
end
RUBY

assert_equal(1, response.size)
assert_equal("FooJob", response[0].name)
assert_empty(response[0].children)
end

private

def generate_document_symbols_for_source(source)
Expand Down