Class: RubyLsp::Rails::CodeLens

Inherits:
Object
  • Object
show all
Includes:
Requests::Support::Common, ActiveSupportTestCaseHelper
Defined in:
lib/ruby_lsp/ruby_lsp_rails/code_lens.rb

Overview

![CodeLens demo](../../code_lens.gif)

This feature adds Code Lens features for Rails applications.

For Active Support test cases:

  • Run tests in the VS Terminal

  • Run tests in the VS Code Test Explorer

  • Debug tests

  • Run migrations in the VS Terminal

For Rails controllers:

  • See the path corresponding to an action

  • Click on the action’s Code Lens to jump to its declaration in the routes.

Note: This depends on a support for the ‘rubyLsp.openFile` command. For the VS Code extension this is built-in, but for other editors this may require some custom configuration.

The [code lens](microsoft.github.io/language-server-protocol/specification#textDocument_codeLens) request informs the editor of runnable commands such as tests. It’s available for tests which inherit from ‘ActiveSupport::TestCase` or one of its descendants, such as `ActionDispatch::IntegrationTest`.

# Example:

For the following code, Code Lenses will be added above the class definition above each test method.

“‘ruby Run class HelloTest < ActiveSupport::TestCase # <- Will show code lenses above for running or debugging the whole test

test "outputs hello" do # <- Will show code lenses above for running or debugging this test
  # ...
end

test "outputs goodbye" do # <- Will show code lenses above for running or debugging this test
  # ...
end

end “‘

# Example: “‘ruby Run class AddFirstNameToUsers < ActiveRecord::Migration

# ...

end “‘

The code lenses will be displayed above the class and above each test method.

Note: When using the Test Explorer view, if your code contains a statement to pause execution (e.g. ‘debugger`) it will cause the test runner to hang.

For the following code, assuming the routing contains ‘resources :users`, a Code Lens will be seen above each action.

“‘ruby class UsersController < ApplicationController

GET /users(.:format)
def index # <- Will show code lens above for the path
end

end “‘

Note: Complex routing configurations may not be supported.

Instance Method Summary collapse

Methods included from ActiveSupportTestCaseHelper

#extract_test_case_name

Constructor Details

#initialize(client, global_state, response_builder, uri, dispatcher) ⇒ CodeLens

: (RunnerClient, GlobalState, ResponseBuilders::CollectionResponseBuilder, URI::Generic, Prism::Dispatcher) -> void



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/ruby_lsp/ruby_lsp_rails/code_lens.rb', line 79

def initialize(client, global_state, response_builder, uri, dispatcher)
  @client = client
  @global_state = global_state
  @response_builder = response_builder
  @path = uri.to_standardized_path #: String?
  @group_id = 1 #: Integer
  @group_id_stack = [] #: Array[Integer]
  @constant_name_stack = [] #: Array[[String, String?]]

  dispatcher.register(
    self,
    :on_call_node_enter,
    :on_class_node_enter,
    :on_def_node_enter,
    :on_class_node_leave,
    :on_module_node_enter,
    :on_module_node_leave,
  )
end

Instance Method Details

#on_call_node_enter(node) ⇒ Object

: (Prism::CallNode node) -> void



100
101
102
103
104
105
106
107
108
109
110
# File 'lib/ruby_lsp/ruby_lsp_rails/code_lens.rb', line 100

def on_call_node_enter(node)
  # Remove this method once the rollout is complete
  return if @global_state.enabled_feature?(:fullTestDiscovery)

  content = extract_test_case_name(node)
  return unless content

  line_number = node.location.start_line
  command = "#{test_command} #{@path}:#{line_number}"
  add_test_code_lens(node, name: content, command: command, kind: :example)
end

#on_class_node_enter(node) ⇒ Object

: (Prism::ClassNode node) -> void



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/ruby_lsp/ruby_lsp_rails/code_lens.rb', line 133

def on_class_node_enter(node)
  class_name = node.constant_path.slice
  superclass_name = node.superclass&.slice

  # We need to use a stack because someone could define a nested class
  # inside a controller. When we exit that nested class declaration, we are
  # back in a controller context. This part is used in other places in the LSP
  @constant_name_stack << [class_name, superclass_name]

  # Remove this entire if block once the rollout is complete
  if class_name.end_with?("Test") && !@global_state.enabled_feature?(:fullTestDiscovery)
    fully_qualified_name = @constant_name_stack.map(&:first).join("::")
    command = "#{test_command} #{@path} --name \"/#{Shellwords.escape(fully_qualified_name)}(#|::)/\""
    add_test_code_lens(node, name: class_name, command: command, kind: :group)
    @group_id_stack.push(@group_id)
    @group_id += 1
  end

  if @path && superclass_name&.start_with?("ActiveRecord::Migration")
    command = "#{migrate_command} VERSION=#{migration_version}"
    add_migrate_code_lens(node, name: class_name, command: command)
  end
end

#on_class_node_leave(node) ⇒ Object

: (Prism::ClassNode node) -> void



158
159
160
161
162
163
164
165
166
167
168
# File 'lib/ruby_lsp/ruby_lsp_rails/code_lens.rb', line 158

def on_class_node_leave(node)
  class_name = node.constant_path.slice

  if class_name.end_with?("Test")
    @group_id_stack.pop
  end
  # Remove everything but the `@constant_name_stack.pop` once the rollout is complete
  return if @global_state.enabled_feature?(:fullTestDiscovery)

  @constant_name_stack.pop
end

#on_def_node_enter(node) ⇒ Object

Although uncommon, Rails tests can be written with the classic “def test_name” syntax. : (Prism::DefNode node) -> void



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/ruby_lsp/ruby_lsp_rails/code_lens.rb', line 114

def on_def_node_enter(node)
  # Remove this entire unless block once the rollout is complete
  unless @global_state.enabled_feature?(:fullTestDiscovery)
    method_name = node.name.to_s

    if method_name.start_with?("test_")
      line_number = node.location.start_line
      command = "#{test_command} #{@path}:#{line_number}"
      add_test_code_lens(node, name: method_name, command: command, kind: :example)
    end
  end

  if controller?
    add_route_code_lens_to_action(node)
    add_jump_to_view(node)
  end
end

#on_module_node_enter(node) ⇒ Object

: (Prism::ModuleNode node) -> void



171
172
173
# File 'lib/ruby_lsp/ruby_lsp_rails/code_lens.rb', line 171

def on_module_node_enter(node)
  @constant_name_stack << [node.constant_path.slice, nil]
end

#on_module_node_leave(node) ⇒ Object

: (Prism::ModuleNode node) -> void



176
177
178
# File 'lib/ruby_lsp/ruby_lsp_rails/code_lens.rb', line 176

def on_module_node_leave(node)
  @constant_name_stack.pop
end