diff --git a/lib/ruby_lsp/listeners/code_lens.rb b/lib/ruby_lsp/listeners/code_lens.rb index 744d3e597..0c98ca2ec 100644 --- a/lib/ruby_lsp/listeners/code_lens.rb +++ b/lib/ruby_lsp/listeners/code_lens.rb @@ -21,7 +21,8 @@ class CodeLens < Listener ) ACCESS_MODIFIERS = T.let([:public, :private, :protected], T::Array[Symbol]) SUPPORTED_TEST_LIBRARIES = T.let(["minitest", "test-unit"], T::Array[String]) - + DESCRIBE_KEYWORD = T.let(:describe, Symbol) + IT_KEYWORD = T.let(:it, Symbol) ResponseType = type_member { { fixed: T::Array[Interface::CodeLens] } } sig { override.returns(ResponseType) } @@ -40,7 +41,7 @@ def initialize(uri, lenses_configuration, dispatcher) @path = T.let(uri.to_standardized_path, T.nilable(String)) # visibility_stack is a stack of [current_visibility, previous_visibility] @visibility_stack = T.let([[:public, :public]], T::Array[T::Array[T.nilable(Symbol)]]) - @class_stack = T.let([], T::Array[String]) + @group_stack = T.let([], T::Array[String]) @group_id = T.let(1, Integer) @group_id_stack = T.let([], T::Array[Integer]) @lenses_configuration = lenses_configuration @@ -61,13 +62,13 @@ def initialize(uri, lenses_configuration, dispatcher) def on_class_node_enter(node) @visibility_stack.push([:public, :public]) class_name = node.constant_path.slice - @class_stack.push(class_name) + @group_stack.push(class_name) if @path && class_name.end_with?("Test") add_test_code_lens( node, name: class_name, - command: generate_test_command(class_name: class_name), + command: generate_test_command(group_name: class_name), kind: :group, ) end @@ -79,13 +80,13 @@ def on_class_node_enter(node) sig { params(node: Prism::ClassNode).void } def on_class_node_leave(node) @visibility_stack.pop - @class_stack.pop + @group_stack.pop @group_id_stack.pop end sig { params(node: Prism::DefNode).void } def on_def_node_enter(node) - class_name = @class_stack.last + class_name = @group_stack.last return unless class_name&.end_with?("Test") visibility, _ = @visibility_stack.last @@ -95,7 +96,7 @@ def on_def_node_enter(node) add_test_code_lens( node, name: method_name, - command: generate_test_command(method_name: method_name, class_name: class_name), + command: generate_test_command(method_name: method_name, group_name: class_name), kind: :example, ) end @@ -120,6 +121,19 @@ def on_call_node_enter(node) return end + if [DESCRIBE_KEYWORD, IT_KEYWORD].include?(name) + case name + when DESCRIBE_KEYWORD + add_spec_code_lens(node, kind: :group) + @group_id_stack.push(@group_id) + @group_id += 1 + when IT_KEYWORD + add_spec_code_lens(node, kind: :example) + end + + return + end + if @path&.include?(GEMFILE_NAME) && name == :gem && arguments return unless @lenses_configuration.enabled?(:gemfileLinks) @@ -137,6 +151,9 @@ def on_call_node_enter(node) def on_call_node_leave(node) _, prev_visibility = @visibility_stack.pop @visibility_stack.push([prev_visibility, prev_visibility]) + if node.name == DESCRIBE_KEYWORD + @group_id_stack.pop + end end private @@ -196,19 +213,19 @@ def resolve_gem_remote(gem_name) end end - sig { params(class_name: String, method_name: T.nilable(String)).returns(String) } - def generate_test_command(class_name:, method_name: nil) + sig { params(group_name: String, method_name: T.nilable(String)).returns(String) } + def generate_test_command(group_name:, method_name: nil) command = BASE_COMMAND + T.must(@path) case DependencyDetector.instance.detected_test_library when "minitest" command += if method_name - " --name " + "/#{Shellwords.escape(class_name + "#" + method_name)}/" + " --name " + "/#{Shellwords.escape(group_name + "#" + method_name)}/" else - " --name " + "/#{Shellwords.escape(class_name)}/" + " --name " + "/#{Shellwords.escape(group_name)}/" end when "test-unit" - command += " --testcase " + "/#{Shellwords.escape(class_name)}/" + command += " --testcase " + "/#{Shellwords.escape(group_name)}/" if method_name command += " --name " + Shellwords.escape(method_name) @@ -228,6 +245,31 @@ def add_open_gem_remote_code_lens(node, remote) data: { type: "link" }, ) end + + sig { params(node: Prism::CallNode, kind: Symbol).void } + def add_spec_code_lens(node, kind:) + arguments = node.arguments + return unless arguments + + first_argument = arguments.arguments.first + return unless first_argument + + name = case first_argument + when Prism::StringNode + first_argument.content + when Prism::ConstantReadNode + first_argument.full_name + end + + return unless name + + add_test_code_lens( + node, + name: name, + command: generate_test_command(group_name: name), + kind: kind, + ) + end end end end diff --git a/test/expectations/code_lens/minitest_spec_tests.exp.json b/test/expectations/code_lens/minitest_spec_tests.exp.json new file mode 100644 index 000000000..7498faac6 --- /dev/null +++ b/test/expectations/code_lens/minitest_spec_tests.exp.json @@ -0,0 +1,782 @@ +{ + "result": [ + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 14, + "character": 3 + } + }, + "command": { + "title": "Run", + "command": "rubyLsp.runTest", + "arguments": [ + "/fixtures/minitest_spec_tests.rb", + "Foo", + "bundle exec ruby -Itest /fixtures/minitest_spec_tests.rb --name /Foo/", + { + "start_line": 0, + "start_column": 0, + "end_line": 14, + "end_column": 3 + } + ] + }, + "data": { + "type": "test", + "group_id": null, + "kind": "group", + "id": 1 + } + }, + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 14, + "character": 3 + } + }, + "command": { + "title": "Run In Terminal", + "command": "rubyLsp.runTestInTerminal", + "arguments": [ + "/fixtures/minitest_spec_tests.rb", + "Foo", + "bundle exec ruby -Itest /fixtures/minitest_spec_tests.rb --name /Foo/", + { + "start_line": 0, + "start_column": 0, + "end_line": 14, + "end_column": 3 + } + ] + }, + "data": { + "type": "test_in_terminal", + "group_id": null, + "kind": "group", + "id": 1 + } + }, + { + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 14, + "character": 3 + } + }, + "command": { + "title": "Debug", + "command": "rubyLsp.debugTest", + "arguments": [ + "/fixtures/minitest_spec_tests.rb", + "Foo", + "bundle exec ruby -Itest /fixtures/minitest_spec_tests.rb --name /Foo/", + { + "start_line": 0, + "start_column": 0, + "end_line": 14, + "end_column": 3 + } + ] + }, + "data": { + "type": "debug", + "group_id": null, + "kind": "group", + "id": 1 + } + }, + { + "range": { + "start": { + "line": 1, + "character": 2 + }, + "end": { + "line": 1, + "character": 19 + } + }, + "command": { + "title": "Run", + "command": "rubyLsp.runTest", + "arguments": [ + "/fixtures/minitest_spec_tests.rb", + "it_level_one", + "bundle exec ruby -Itest /fixtures/minitest_spec_tests.rb --name /it_level_one/", + { + "start_line": 1, + "start_column": 2, + "end_line": 1, + "end_column": 19 + } + ] + }, + "data": { + "type": "test", + "group_id": 1, + "kind": "example" + } + }, + { + "range": { + "start": { + "line": 1, + "character": 2 + }, + "end": { + "line": 1, + "character": 19 + } + }, + "command": { + "title": "Run In Terminal", + "command": "rubyLsp.runTestInTerminal", + "arguments": [ + "/fixtures/minitest_spec_tests.rb", + "it_level_one", + "bundle exec ruby -Itest /fixtures/minitest_spec_tests.rb --name /it_level_one/", + { + "start_line": 1, + "start_column": 2, + "end_line": 1, + "end_column": 19 + } + ] + }, + "data": { + "type": "test_in_terminal", + "group_id": 1, + "kind": "example" + } + }, + { + "range": { + "start": { + "line": 1, + "character": 2 + }, + "end": { + "line": 1, + "character": 19 + } + }, + "command": { + "title": "Debug", + "command": "rubyLsp.debugTest", + "arguments": [ + "/fixtures/minitest_spec_tests.rb", + "it_level_one", + "bundle exec ruby -Itest /fixtures/minitest_spec_tests.rb --name /it_level_one/", + { + "start_line": 1, + "start_column": 2, + "end_line": 1, + "end_column": 19 + } + ] + }, + "data": { + "type": "debug", + "group_id": 1, + "kind": "example" + } + }, + { + "range": { + "start": { + "line": 3, + "character": 2 + }, + "end": { + "line": 11, + "character": 5 + } + }, + "command": { + "title": "Run", + "command": "rubyLsp.runTest", + "arguments": [ + "/fixtures/minitest_spec_tests.rb", + "nested", + "bundle exec ruby -Itest /fixtures/minitest_spec_tests.rb --name /nested/", + { + "start_line": 3, + "start_column": 2, + "end_line": 11, + "end_column": 5 + } + ] + }, + "data": { + "type": "test", + "group_id": 1, + "kind": "group", + "id": 2 + } + }, + { + "range": { + "start": { + "line": 3, + "character": 2 + }, + "end": { + "line": 11, + "character": 5 + } + }, + "command": { + "title": "Run In Terminal", + "command": "rubyLsp.runTestInTerminal", + "arguments": [ + "/fixtures/minitest_spec_tests.rb", + "nested", + "bundle exec ruby -Itest /fixtures/minitest_spec_tests.rb --name /nested/", + { + "start_line": 3, + "start_column": 2, + "end_line": 11, + "end_column": 5 + } + ] + }, + "data": { + "type": "test_in_terminal", + "group_id": 1, + "kind": "group", + "id": 2 + } + }, + { + "range": { + "start": { + "line": 3, + "character": 2 + }, + "end": { + "line": 11, + "character": 5 + } + }, + "command": { + "title": "Debug", + "command": "rubyLsp.debugTest", + "arguments": [ + "/fixtures/minitest_spec_tests.rb", + "nested", + "bundle exec ruby -Itest /fixtures/minitest_spec_tests.rb --name /nested/", + { + "start_line": 3, + "start_column": 2, + "end_line": 11, + "end_column": 5 + } + ] + }, + "data": { + "type": "debug", + "group_id": 1, + "kind": "group", + "id": 2 + } + }, + { + "range": { + "start": { + "line": 4, + "character": 4 + }, + "end": { + "line": 4, + "character": 18 + } + }, + "command": { + "title": "Run", + "command": "rubyLsp.runTest", + "arguments": [ + "/fixtures/minitest_spec_tests.rb", + "it_nested", + "bundle exec ruby -Itest /fixtures/minitest_spec_tests.rb --name /it_nested/", + { + "start_line": 4, + "start_column": 4, + "end_line": 4, + "end_column": 18 + } + ] + }, + "data": { + "type": "test", + "group_id": 2, + "kind": "example" + } + }, + { + "range": { + "start": { + "line": 4, + "character": 4 + }, + "end": { + "line": 4, + "character": 18 + } + }, + "command": { + "title": "Run In Terminal", + "command": "rubyLsp.runTestInTerminal", + "arguments": [ + "/fixtures/minitest_spec_tests.rb", + "it_nested", + "bundle exec ruby -Itest /fixtures/minitest_spec_tests.rb --name /it_nested/", + { + "start_line": 4, + "start_column": 4, + "end_line": 4, + "end_column": 18 + } + ] + }, + "data": { + "type": "test_in_terminal", + "group_id": 2, + "kind": "example" + } + }, + { + "range": { + "start": { + "line": 4, + "character": 4 + }, + "end": { + "line": 4, + "character": 18 + } + }, + "command": { + "title": "Debug", + "command": "rubyLsp.debugTest", + "arguments": [ + "/fixtures/minitest_spec_tests.rb", + "it_nested", + "bundle exec ruby -Itest /fixtures/minitest_spec_tests.rb --name /it_nested/", + { + "start_line": 4, + "start_column": 4, + "end_line": 4, + "end_column": 18 + } + ] + }, + "data": { + "type": "debug", + "group_id": 2, + "kind": "example" + } + }, + { + "range": { + "start": { + "line": 6, + "character": 4 + }, + "end": { + "line": 8, + "character": 7 + } + }, + "command": { + "title": "Run", + "command": "rubyLsp.runTest", + "arguments": [ + "/fixtures/minitest_spec_tests.rb", + "deep_nested", + "bundle exec ruby -Itest /fixtures/minitest_spec_tests.rb --name /deep_nested/", + { + "start_line": 6, + "start_column": 4, + "end_line": 8, + "end_column": 7 + } + ] + }, + "data": { + "type": "test", + "group_id": 2, + "kind": "group", + "id": 3 + } + }, + { + "range": { + "start": { + "line": 6, + "character": 4 + }, + "end": { + "line": 8, + "character": 7 + } + }, + "command": { + "title": "Run In Terminal", + "command": "rubyLsp.runTestInTerminal", + "arguments": [ + "/fixtures/minitest_spec_tests.rb", + "deep_nested", + "bundle exec ruby -Itest /fixtures/minitest_spec_tests.rb --name /deep_nested/", + { + "start_line": 6, + "start_column": 4, + "end_line": 8, + "end_column": 7 + } + ] + }, + "data": { + "type": "test_in_terminal", + "group_id": 2, + "kind": "group", + "id": 3 + } + }, + { + "range": { + "start": { + "line": 6, + "character": 4 + }, + "end": { + "line": 8, + "character": 7 + } + }, + "command": { + "title": "Debug", + "command": "rubyLsp.debugTest", + "arguments": [ + "/fixtures/minitest_spec_tests.rb", + "deep_nested", + "bundle exec ruby -Itest /fixtures/minitest_spec_tests.rb --name /deep_nested/", + { + "start_line": 6, + "start_column": 4, + "end_line": 8, + "end_column": 7 + } + ] + }, + "data": { + "type": "debug", + "group_id": 2, + "kind": "group", + "id": 3 + } + }, + { + "range": { + "start": { + "line": 7, + "character": 6 + }, + "end": { + "line": 7, + "character": 25 + } + }, + "command": { + "title": "Run", + "command": "rubyLsp.runTest", + "arguments": [ + "/fixtures/minitest_spec_tests.rb", + "it_deep_nested", + "bundle exec ruby -Itest /fixtures/minitest_spec_tests.rb --name /it_deep_nested/", + { + "start_line": 7, + "start_column": 6, + "end_line": 7, + "end_column": 25 + } + ] + }, + "data": { + "type": "test", + "group_id": 3, + "kind": "example" + } + }, + { + "range": { + "start": { + "line": 7, + "character": 6 + }, + "end": { + "line": 7, + "character": 25 + } + }, + "command": { + "title": "Run In Terminal", + "command": "rubyLsp.runTestInTerminal", + "arguments": [ + "/fixtures/minitest_spec_tests.rb", + "it_deep_nested", + "bundle exec ruby -Itest /fixtures/minitest_spec_tests.rb --name /it_deep_nested/", + { + "start_line": 7, + "start_column": 6, + "end_line": 7, + "end_column": 25 + } + ] + }, + "data": { + "type": "test_in_terminal", + "group_id": 3, + "kind": "example" + } + }, + { + "range": { + "start": { + "line": 7, + "character": 6 + }, + "end": { + "line": 7, + "character": 25 + } + }, + "command": { + "title": "Debug", + "command": "rubyLsp.debugTest", + "arguments": [ + "/fixtures/minitest_spec_tests.rb", + "it_deep_nested", + "bundle exec ruby -Itest /fixtures/minitest_spec_tests.rb --name /it_deep_nested/", + { + "start_line": 7, + "start_column": 6, + "end_line": 7, + "end_column": 25 + } + ] + }, + "data": { + "type": "debug", + "group_id": 3, + "kind": "example" + } + }, + { + "range": { + "start": { + "line": 10, + "character": 4 + }, + "end": { + "line": 10, + "character": 24 + } + }, + "command": { + "title": "Run", + "command": "rubyLsp.runTest", + "arguments": [ + "/fixtures/minitest_spec_tests.rb", + "it_nested_again", + "bundle exec ruby -Itest /fixtures/minitest_spec_tests.rb --name /it_nested_again/", + { + "start_line": 10, + "start_column": 4, + "end_line": 10, + "end_column": 24 + } + ] + }, + "data": { + "type": "test", + "group_id": 2, + "kind": "example" + } + }, + { + "range": { + "start": { + "line": 10, + "character": 4 + }, + "end": { + "line": 10, + "character": 24 + } + }, + "command": { + "title": "Run In Terminal", + "command": "rubyLsp.runTestInTerminal", + "arguments": [ + "/fixtures/minitest_spec_tests.rb", + "it_nested_again", + "bundle exec ruby -Itest /fixtures/minitest_spec_tests.rb --name /it_nested_again/", + { + "start_line": 10, + "start_column": 4, + "end_line": 10, + "end_column": 24 + } + ] + }, + "data": { + "type": "test_in_terminal", + "group_id": 2, + "kind": "example" + } + }, + { + "range": { + "start": { + "line": 10, + "character": 4 + }, + "end": { + "line": 10, + "character": 24 + } + }, + "command": { + "title": "Debug", + "command": "rubyLsp.debugTest", + "arguments": [ + "/fixtures/minitest_spec_tests.rb", + "it_nested_again", + "bundle exec ruby -Itest /fixtures/minitest_spec_tests.rb --name /it_nested_again/", + { + "start_line": 10, + "start_column": 4, + "end_line": 10, + "end_column": 24 + } + ] + }, + "data": { + "type": "debug", + "group_id": 2, + "kind": "example" + } + }, + { + "range": { + "start": { + "line": 13, + "character": 2 + }, + "end": { + "line": 13, + "character": 25 + } + }, + "command": { + "title": "Run", + "command": "rubyLsp.runTest", + "arguments": [ + "/fixtures/minitest_spec_tests.rb", + "it_level_one_again", + "bundle exec ruby -Itest /fixtures/minitest_spec_tests.rb --name /it_level_one_again/", + { + "start_line": 13, + "start_column": 2, + "end_line": 13, + "end_column": 25 + } + ] + }, + "data": { + "type": "test", + "group_id": 1, + "kind": "example" + } + }, + { + "range": { + "start": { + "line": 13, + "character": 2 + }, + "end": { + "line": 13, + "character": 25 + } + }, + "command": { + "title": "Run In Terminal", + "command": "rubyLsp.runTestInTerminal", + "arguments": [ + "/fixtures/minitest_spec_tests.rb", + "it_level_one_again", + "bundle exec ruby -Itest /fixtures/minitest_spec_tests.rb --name /it_level_one_again/", + { + "start_line": 13, + "start_column": 2, + "end_line": 13, + "end_column": 25 + } + ] + }, + "data": { + "type": "test_in_terminal", + "group_id": 1, + "kind": "example" + } + }, + { + "range": { + "start": { + "line": 13, + "character": 2 + }, + "end": { + "line": 13, + "character": 25 + } + }, + "command": { + "title": "Debug", + "command": "rubyLsp.debugTest", + "arguments": [ + "/fixtures/minitest_spec_tests.rb", + "it_level_one_again", + "bundle exec ruby -Itest /fixtures/minitest_spec_tests.rb --name /it_level_one_again/", + { + "start_line": 13, + "start_column": 2, + "end_line": 13, + "end_column": 25 + } + ] + }, + "data": { + "type": "debug", + "group_id": 1, + "kind": "example" + } + } + ], + "params": [] +} diff --git a/test/fixtures/minitest_spec_tests.rb b/test/fixtures/minitest_spec_tests.rb new file mode 100644 index 000000000..beefdaf49 --- /dev/null +++ b/test/fixtures/minitest_spec_tests.rb @@ -0,0 +1,15 @@ +describe Foo do + it "it_level_one" + + describe "nested" do + it "it_nested" + + describe "deep_nested" do + it "it_deep_nested" + end + + it "it_nested_again" + end + + it "it_level_one_again" +end