|
| 1 | +# typed: strict |
| 2 | +# frozen_string_literal: true |
| 3 | + |
| 4 | +module RubyLsp |
| 5 | + module Rails |
| 6 | + #  |
| 7 | + # |
| 8 | + # This feature adds several CodeLens features for Rails applications using Active Support test cases: |
| 9 | + # - Run tests in the VS Terminal |
| 10 | + # - Run tests in the VS Code Test Explorer |
| 11 | + # - Debug tests |
| 12 | + # |
| 13 | + # The |
| 14 | + # [code lens](https://microsoft.github.io/language-server-protocol/specification#textDocument_codeLens) |
| 15 | + # request informs the editor of runnable commands such as tests |
| 16 | + # |
| 17 | + # # Example: |
| 18 | + # |
| 19 | + # For the following code, Code Lenses will be added above the class definition above each test method. |
| 20 | + # |
| 21 | + # ```ruby |
| 22 | + # Run |
| 23 | + # class HelloTest < ActiveSupport::TestCase # <- Will show code lenses above for running or debugging the whole test |
| 24 | + # test "outputs hello" do # <- Will show code lenses above for running or debugging this test |
| 25 | + # # ... |
| 26 | + # end |
| 27 | + # |
| 28 | + # test "outputs goodbye" do # <- Will show code lenses above for running or debugging this test |
| 29 | + # # ... |
| 30 | + # end |
| 31 | + # end |
| 32 | + # ```` |
| 33 | + # |
| 34 | + # The code lenses will be displayed above the class and above each test method. |
| 35 | + class CodeLens < ::RubyLsp::Listener |
| 36 | + extend T::Sig |
| 37 | + extend T::Generic |
| 38 | + |
| 39 | + ResponseType = type_member { { fixed: T::Array[::RubyLsp::Interface::CodeLens] } } |
| 40 | + BASE_COMMAND = "bin/rails test" |
| 41 | + |
| 42 | + ::RubyLsp::Requests::CodeLens.add_listener(self) |
| 43 | + |
| 44 | + sig { override.returns(ResponseType) } |
| 45 | + attr_reader :response |
| 46 | + |
| 47 | + sig { params(uri: String, emitter: EventEmitter, message_queue: Thread::Queue).void } |
| 48 | + def initialize(uri, emitter, message_queue) |
| 49 | + @response = T.let([], ResponseType) |
| 50 | + @path = T.let(URI(uri).path, T.nilable(String)) |
| 51 | + emitter.register(self, :on_command, :on_class, :on_def) |
| 52 | + |
| 53 | + super(emitter, message_queue) |
| 54 | + end |
| 55 | + |
| 56 | + sig { params(node: SyntaxTree::Command).void } |
| 57 | + def on_command(node) |
| 58 | + message_value = node.message.value |
| 59 | + return unless message_value == "test" && node.arguments.parts.any? |
| 60 | + |
| 61 | + first_argument = node.arguments.parts.first |
| 62 | + return unless first_argument.is_a?(SyntaxTree::StringLiteral) |
| 63 | + |
| 64 | + test_name = first_argument.parts.first.value |
| 65 | + return unless test_name |
| 66 | + |
| 67 | + line_number = node.location.start_line |
| 68 | + command = "#{BASE_COMMAND} #{@path}:#{line_number}" |
| 69 | + add_test_code_lens(node, name: test_name, command: command, kind: :example) |
| 70 | + end |
| 71 | + |
| 72 | + # Although uncommon, Rails tests can be written with the classic "def test_name" syntax. |
| 73 | + sig { params(node: SyntaxTree::DefNode).void } |
| 74 | + def on_def(node) |
| 75 | + method_name = node.name.value |
| 76 | + if method_name.start_with?("test_") |
| 77 | + line_number = node.location.start_line |
| 78 | + command = "#{BASE_COMMAND} #{@path}:#{line_number}" |
| 79 | + add_test_code_lens(node, name: method_name, command: command, kind: :example) |
| 80 | + end |
| 81 | + end |
| 82 | + |
| 83 | + sig { params(node: SyntaxTree::ClassDeclaration).void } |
| 84 | + def on_class(node) |
| 85 | + class_name = node.constant.constant.value |
| 86 | + if class_name.end_with?("Test") |
| 87 | + command = "#{BASE_COMMAND} #{@path}" |
| 88 | + add_test_code_lens(node, name: class_name, command: command, kind: :group) |
| 89 | + end |
| 90 | + end |
| 91 | + |
| 92 | + private |
| 93 | + |
| 94 | + sig { params(node: SyntaxTree::Node, name: String, command: String, kind: Symbol).void } |
| 95 | + def add_test_code_lens(node, name:, command:, kind:) |
| 96 | + arguments = [ |
| 97 | + @path, |
| 98 | + name, |
| 99 | + command, |
| 100 | + { |
| 101 | + start_line: node.location.start_line - 1, |
| 102 | + start_column: node.location.start_column, |
| 103 | + end_line: node.location.end_line - 1, |
| 104 | + end_column: node.location.end_column, |
| 105 | + }, |
| 106 | + ] |
| 107 | + |
| 108 | + @response << create_code_lens( |
| 109 | + node, |
| 110 | + title: "Run", |
| 111 | + command_name: "rubyLsp.runTest", |
| 112 | + arguments: arguments, |
| 113 | + data: { type: "test", kind: kind }, |
| 114 | + ) |
| 115 | + |
| 116 | + @response << create_code_lens( |
| 117 | + node, |
| 118 | + title: "Run In Terminal", |
| 119 | + command_name: "rubyLsp.runTestInTerminal", |
| 120 | + arguments: arguments, |
| 121 | + data: { type: "test_in_terminal", kind: kind }, |
| 122 | + ) |
| 123 | + |
| 124 | + @response << create_code_lens( |
| 125 | + node, |
| 126 | + title: "Debug", |
| 127 | + command_name: "rubyLsp.debugTest", |
| 128 | + arguments: arguments, |
| 129 | + data: { type: "debug", kind: kind }, |
| 130 | + ) |
| 131 | + end |
| 132 | + end |
| 133 | + end |
| 134 | +end |
0 commit comments