Skip to content

Commit

Permalink
Support configuring custom rspec command in VS Code (#57)
Browse files Browse the repository at this point in the history
* Add support configuring rspec command and relative path

* Fix command generation and type issues

* Add specs to exercise new configuration options

* Add documentation for new configuration items

* Add debug option and clarify preferred dev container strategy

This commit adds an option (consumed from VS Code settings) that
allows a debug mode to troubleshoot configuration issues.

Further, this commit clarifies the preferred approach for using
this addon with Dev Containers.

Projects that use Docker or other container technologies for development
should use a the VS Code Dev Containers extension to run Ruby LSP _within_
the dev Container.

This prevents situations where the spec paths provided to rspec are
host machine paths rather than container paths.

* Correct linting errors
  • Loading branch information
westonkd authored Feb 3, 2025
1 parent 2b1bd2a commit b590aac
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 3 deletions.
77 changes: 77 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,83 @@ In VS Code this feature can be triggered by one of the following methods:
<img src="misc/go-to-definition.gif" alt="Go to definition" width="75%">

### VS Code Configuration
`ruby-lsp-rspec` supports various configuration items exposed via `settings.json` in VS Code.

These configuration options require the `ruby-lsp` VS Code plugin are nested within `rubyLsp.addonSettings`:
```json
{
...
"rubyLsp.addonSettings": {
"Ruby LSP RSpec": {
// Configuration goes here
}
}
}
```

#### `rspecCommand`
**Description:**
Override the inferred rspec command with a user-specified command

**Default Value**: `nil` (infer rspec command based on presence of a binstub or Gemfile)

**Example**
```json
{
...
"rubyLsp.addonSettings": {
"Ruby LSP RSpec": {
"rspecCommand": "rspec -f d",
}
}
}
```

#### `debug`
**Description:**
A boolean flag that prints the complete RSpec command to stdout when enabled.

View the output in VS Code's `OUTPUT` panel under `Ruby LSP`.

**Default Value**: `false`

**Example**
```json
{
...
"rubyLsp.addonSettings": {
"Ruby LSP RSpec": {
"debug": true
}
}
}
```

#### Developing on containers
If your project uses containers for development, you should use `Visual Studio Code Dev Containers` extension.

This extension will run Ruby LSP (and thus Ruby LSP RSpec) within the Dev Container, which allows the proper spec paths to be sent to rspec.

For more details on using Ruby LSP with containers and setting up the dev continers extension, see [the Ruby LSP documentation](https://github.com/Shopify/ruby-lsp/blob/main/vscode/README.md?tab=readme-ov-file#developing-on-containers).

Be sure to specify Ruby LSP as an extension that should run _within_ the Dev Container in your `.devcontainer.json`:
```json
{
"name": "my-app",
...
"customizations": {
"vscode": {
"extensions": [
"Shopify.ruby-lsp"
...
]
}
}
}
```


## Development

After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
Expand Down
19 changes: 18 additions & 1 deletion lib/ruby_lsp/ruby_lsp_rspec/addon.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,26 @@ module RSpec
class Addon < ::RubyLsp::Addon
extend T::Sig

sig { returns(T.nilable(String)) }
attr_reader :rspec_command

sig { returns(T::Boolean) }
attr_reader :debug

sig { void }
def initialize
super
@debug = T.let(false, T::Boolean)
@rspec_command = T.let(nil, T.nilable(String))
end

sig { override.params(global_state: GlobalState, message_queue: Thread::Queue).void }
def activate(global_state, message_queue)
@index = T.let(global_state.index, T.nilable(RubyIndexer::Index))

settings = global_state.settings_for_addon(name)
@rspec_command = T.let(settings&.dig(:rspecCommand), T.nilable(String))
@debug = settings&.dig(:debug) || false
end

sig { override.void }
Expand All @@ -38,7 +55,7 @@ def version
def create_code_lens_listener(response_builder, uri, dispatcher)
return unless uri.to_standardized_path&.end_with?("_test.rb") || uri.to_standardized_path&.end_with?("_spec.rb")

CodeLens.new(response_builder, uri, dispatcher)
CodeLens.new(response_builder, uri, dispatcher, rspec_command: rspec_command, debug: debug)
end

sig do
Expand Down
15 changes: 13 additions & 2 deletions lib/ruby_lsp/ruby_lsp_rspec/code_lens.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ class CodeLens
response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::CodeLens],
uri: URI::Generic,
dispatcher: Prism::Dispatcher,
rspec_command: T.nilable(String),
debug: T::Boolean,
).void
end
def initialize(response_builder, uri, dispatcher)
def initialize(response_builder, uri, dispatcher, rspec_command: nil, debug: false)
@response_builder = response_builder
# Listener is only initialized if uri.to_standardized_path is valid
@path = T.let(T.must(uri.to_standardized_path), String)
Expand All @@ -24,8 +26,10 @@ def initialize(response_builder, uri, dispatcher)
@anonymous_example_count = T.let(0, Integer)
dispatcher.register(self, :on_call_node_enter, :on_call_node_leave)

@debug = debug
@base_command = T.let(
begin
# The user-configured command takes precedence over inferred command default
rspec_command || begin
cmd = if File.exist?(File.join(Dir.pwd, "bin", "rspec"))
"bin/rspec"
else
Expand Down Expand Up @@ -71,6 +75,11 @@ def on_call_node_leave(node)

private

sig { params(message: String).void }
def log_message(message)
puts "[#{self.class}]: #{message}"
end

sig { params(node: Prism::CallNode).returns(T::Boolean) }
def valid_group?(node)
!(node.block.nil? || (node.receiver && node.receiver&.slice != "RSpec"))
Expand Down Expand Up @@ -104,6 +113,8 @@ def add_test_code_lens(node, name:, kind:)
line_number = node.location.start_line
command = "#{@base_command} #{@path}:#{line_number}"

log_message("Full command: `#{command}`") if @debug

grouping_data = { group_id: @group_id_stack.last, kind: kind }
grouping_data[:id] = @group_id if kind == :group

Expand Down
37 changes: 37 additions & 0 deletions spec/code_lens_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,43 @@
end
end

context "with a custom rspec command configured" do
let(:configuration) do
{
rspecCommand: "docker compose run --rm web rspec",
}
end

before do
allow_any_instance_of(RubyLsp::GlobalState).to receive(:settings_for_addon).and_return(configuration)
end

it "uses the configured rspec command" do
source = <<~RUBY
RSpec.describe Foo do
it "does something" do
end
end
RUBY

with_server(source, uri) do |server, uri|
server.process_message(
{
id: 1,
method: "textDocument/codeLens",
params: {
textDocument: { uri: uri },
position: { line: 0, character: 0 },
},
},
)

response = pop_result(server).response
expect(response[0].command.arguments[2]).to eq("docker compose run --rm web rspec /fake_spec.rb:1")
end
end
end

context "when the file is not a test file" do
let(:uri) { URI("file:///not_spec_file.rb") }

Expand Down

0 comments on commit b590aac

Please sign in to comment.