Skip to content

Commit b590aac

Browse files
authored
Support configuring custom rspec command in VS Code (#57)
* 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
1 parent 2b1bd2a commit b590aac

File tree

4 files changed

+145
-3
lines changed

4 files changed

+145
-3
lines changed

README.md

+77
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,83 @@ In VS Code this feature can be triggered by one of the following methods:
5656
5757
<img src="misc/go-to-definition.gif" alt="Go to definition" width="75%">
5858

59+
### VS Code Configuration
60+
`ruby-lsp-rspec` supports various configuration items exposed via `settings.json` in VS Code.
61+
62+
These configuration options require the `ruby-lsp` VS Code plugin are nested within `rubyLsp.addonSettings`:
63+
```json
64+
{
65+
...
66+
"rubyLsp.addonSettings": {
67+
"Ruby LSP RSpec": {
68+
// Configuration goes here
69+
}
70+
}
71+
}
72+
```
73+
74+
#### `rspecCommand`
75+
**Description:**
76+
Override the inferred rspec command with a user-specified command
77+
78+
**Default Value**: `nil` (infer rspec command based on presence of a binstub or Gemfile)
79+
80+
**Example**
81+
```json
82+
{
83+
...
84+
"rubyLsp.addonSettings": {
85+
"Ruby LSP RSpec": {
86+
"rspecCommand": "rspec -f d",
87+
}
88+
}
89+
}
90+
```
91+
92+
#### `debug`
93+
**Description:**
94+
A boolean flag that prints the complete RSpec command to stdout when enabled.
95+
96+
View the output in VS Code's `OUTPUT` panel under `Ruby LSP`.
97+
98+
**Default Value**: `false`
99+
100+
**Example**
101+
```json
102+
{
103+
...
104+
"rubyLsp.addonSettings": {
105+
"Ruby LSP RSpec": {
106+
"debug": true
107+
}
108+
}
109+
}
110+
```
111+
112+
#### Developing on containers
113+
If your project uses containers for development, you should use `Visual Studio Code Dev Containers` extension.
114+
115+
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.
116+
117+
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).
118+
119+
Be sure to specify Ruby LSP as an extension that should run _within_ the Dev Container in your `.devcontainer.json`:
120+
```json
121+
{
122+
"name": "my-app",
123+
...
124+
"customizations": {
125+
"vscode": {
126+
"extensions": [
127+
"Shopify.ruby-lsp"
128+
...
129+
]
130+
}
131+
}
132+
}
133+
```
134+
135+
59136
## Development
60137

61138
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.

lib/ruby_lsp/ruby_lsp_rspec/addon.rb

+18-1
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,26 @@ module RSpec
1414
class Addon < ::RubyLsp::Addon
1515
extend T::Sig
1616

17+
sig { returns(T.nilable(String)) }
18+
attr_reader :rspec_command
19+
20+
sig { returns(T::Boolean) }
21+
attr_reader :debug
22+
23+
sig { void }
24+
def initialize
25+
super
26+
@debug = T.let(false, T::Boolean)
27+
@rspec_command = T.let(nil, T.nilable(String))
28+
end
29+
1730
sig { override.params(global_state: GlobalState, message_queue: Thread::Queue).void }
1831
def activate(global_state, message_queue)
1932
@index = T.let(global_state.index, T.nilable(RubyIndexer::Index))
33+
34+
settings = global_state.settings_for_addon(name)
35+
@rspec_command = T.let(settings&.dig(:rspecCommand), T.nilable(String))
36+
@debug = settings&.dig(:debug) || false
2037
end
2138

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

41-
CodeLens.new(response_builder, uri, dispatcher)
58+
CodeLens.new(response_builder, uri, dispatcher, rspec_command: rspec_command, debug: debug)
4259
end
4360

4461
sig do

lib/ruby_lsp/ruby_lsp_rspec/code_lens.rb

+13-2
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ class CodeLens
1313
response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::CodeLens],
1414
uri: URI::Generic,
1515
dispatcher: Prism::Dispatcher,
16+
rspec_command: T.nilable(String),
17+
debug: T::Boolean,
1618
).void
1719
end
18-
def initialize(response_builder, uri, dispatcher)
20+
def initialize(response_builder, uri, dispatcher, rspec_command: nil, debug: false)
1921
@response_builder = response_builder
2022
# Listener is only initialized if uri.to_standardized_path is valid
2123
@path = T.let(T.must(uri.to_standardized_path), String)
@@ -24,8 +26,10 @@ def initialize(response_builder, uri, dispatcher)
2426
@anonymous_example_count = T.let(0, Integer)
2527
dispatcher.register(self, :on_call_node_enter, :on_call_node_leave)
2628

29+
@debug = debug
2730
@base_command = T.let(
28-
begin
31+
# The user-configured command takes precedence over inferred command default
32+
rspec_command || begin
2933
cmd = if File.exist?(File.join(Dir.pwd, "bin", "rspec"))
3034
"bin/rspec"
3135
else
@@ -71,6 +75,11 @@ def on_call_node_leave(node)
7175

7276
private
7377

78+
sig { params(message: String).void }
79+
def log_message(message)
80+
puts "[#{self.class}]: #{message}"
81+
end
82+
7483
sig { params(node: Prism::CallNode).returns(T::Boolean) }
7584
def valid_group?(node)
7685
!(node.block.nil? || (node.receiver && node.receiver&.slice != "RSpec"))
@@ -104,6 +113,8 @@ def add_test_code_lens(node, name:, kind:)
104113
line_number = node.location.start_line
105114
command = "#{@base_command} #{@path}:#{line_number}"
106115

116+
log_message("Full command: `#{command}`") if @debug
117+
107118
grouping_data = { group_id: @group_id_stack.last, kind: kind }
108119
grouping_data[:id] = @group_id if kind == :group
109120

spec/code_lens_spec.rb

+37
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,43 @@
161161
end
162162
end
163163

164+
context "with a custom rspec command configured" do
165+
let(:configuration) do
166+
{
167+
rspecCommand: "docker compose run --rm web rspec",
168+
}
169+
end
170+
171+
before do
172+
allow_any_instance_of(RubyLsp::GlobalState).to receive(:settings_for_addon).and_return(configuration)
173+
end
174+
175+
it "uses the configured rspec command" do
176+
source = <<~RUBY
177+
RSpec.describe Foo do
178+
it "does something" do
179+
end
180+
end
181+
RUBY
182+
183+
with_server(source, uri) do |server, uri|
184+
server.process_message(
185+
{
186+
id: 1,
187+
method: "textDocument/codeLens",
188+
params: {
189+
textDocument: { uri: uri },
190+
position: { line: 0, character: 0 },
191+
},
192+
},
193+
)
194+
195+
response = pop_result(server).response
196+
expect(response[0].command.arguments[2]).to eq("docker compose run --rm web rspec /fake_spec.rb:1")
197+
end
198+
end
199+
end
200+
164201
context "when the file is not a test file" do
165202
let(:uri) { URI("file:///not_spec_file.rb") }
166203

0 commit comments

Comments
 (0)