ruby-lsp-rspec 0.1.21 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5f01b37d539883d5abc213c66cba4c967c362ae8722a9e8c1afb4b8b39c4d069
4
- data.tar.gz: 418b518147829b9770d22b64b9c64ec89efc8f699c6b266be5be29ad08f98bc3
3
+ metadata.gz: 6204c70ec6917f93238449949f16c24892d0145cb10dd32c42819021ef617c83
4
+ data.tar.gz: 510b118788f0469ef5783453042fb5c4f56aef9da2f77f0b1567b77e73d0b959
5
5
  SHA512:
6
- metadata.gz: cf2cad39b55746c3f1078235670b02e0d7dd1840327b873e19919828f3f5ac5e98929958254952089235ef0849cf52d5b2434988a2461bb70eddd406898d37fe
7
- data.tar.gz: cb4e6d74c193c5b0cd876f88d8ae6948563f7c40c988729f9d18f94e951f3517df04e3de94d40dded9e0d5f04b788497272f86443a129385ea4ded8f328b5a60
6
+ metadata.gz: '04947d8bf9659b8d8aa213ea2d305f7d2049719a30aa753e30f289da98b6e41991118681405a3166aac93f5c8d23d4f2ce9f808dcf99d6a1f43aabb85c564980'
7
+ data.tar.gz: 2aab5409230f0783b598fe53511d712e4643f84ec506b63278803ccc93002ca37e981c219041f4fa8b33f768432fa5c2eab13e6941bb76261bddfe9a31b70cb1
data/.rspec CHANGED
@@ -1,3 +1,4 @@
1
1
  --format documentation
2
2
  --color
3
3
  --require spec_helper
4
+ --exclude-pattern **/fixtures/*
data/.rubocop.yml CHANGED
@@ -2,9 +2,11 @@ inherit_gem:
2
2
  rubocop-shopify: rubocop.yml
3
3
 
4
4
  require:
5
- - rubocop-sorbet
6
5
  - rubocop-rake
7
6
 
7
+ plugins:
8
+ - rubocop-sorbet
9
+
8
10
  AllCops:
9
11
  NewCops: disable
10
12
  SuggestExtensions: false
@@ -30,6 +32,7 @@ Sorbet/StrictSigil:
30
32
  - "lib/**/*.rb"
31
33
  Exclude:
32
34
  - "**/*.rake"
35
+ - "lib/ruby_lsp/ruby_lsp_rspec/rspec_formatter.rb"
33
36
  - "spec/**/*.rb"
34
37
 
35
38
  Style/StderrPuts:
data/README.md CHANGED
@@ -15,7 +15,7 @@ group :development do
15
15
  end
16
16
  ```
17
17
 
18
- > [!IMPORTANT]
18
+ > [!IMPORTANT]
19
19
  > Make sure the relevant features are [enabled](https://github.com/Shopify/ruby-lsp/tree/main/vscode#enable-or-disable-features) under your VSCode's `rubyLsp.enabledFeatures` setting, such as `codeLens`.
20
20
 
21
21
  After running `bundle install`, restart Ruby LSP and you should start seeing CodeLens in your RSpec test files.
@@ -56,6 +56,87 @@ In VS Code this feature can be triggered by one of the following methods:
56
56
 
57
57
  <img src="misc/go-to-definition.gif" alt="Go to definition" width="75%">
58
58
 
59
+ ### VS Code Configuration
60
+
61
+ `ruby-lsp-rspec` can be configured through VS Code's `settings.json` file.
62
+
63
+ All configuration options must be nested under the `Ruby LSP RSpec` addon within `rubyLsp.addonSettings`:
64
+
65
+ ```json
66
+ {
67
+ // ...
68
+ "rubyLsp.addonSettings": {
69
+ "Ruby LSP RSpec": {
70
+ // Configuration options go here
71
+ }
72
+ }
73
+ }
74
+ ```
75
+
76
+ #### `rspecCommand`
77
+
78
+ **Description:**
79
+
80
+ Customize the command used to run tests via CodeLens. If not set, the command will be inferred based on the presence of a binstub or Gemfile.
81
+
82
+ **Default Value**: `nil`
83
+
84
+ **Example:**
85
+
86
+ ```json
87
+ {
88
+ // ...
89
+ "rubyLsp.addonSettings": {
90
+ "Ruby LSP RSpec": {
91
+ "rspecCommand": "rspec -f d"
92
+ }
93
+ }
94
+ }
95
+ ```
96
+
97
+ #### `debug`
98
+
99
+ **Description:**
100
+
101
+ Enable debug logging. Currently, this only logs the RSpec command used by CodeLens to stderr, which can be viewed in VS Code's `OUTPUT` panel under `Ruby LSP`.
102
+
103
+ **Default Value**: `false`
104
+
105
+ **Example:**
106
+
107
+ ```json
108
+ {
109
+ "rubyLsp.addonSettings": {
110
+ "Ruby LSP RSpec": {
111
+ "debug": true
112
+ }
113
+ }
114
+ }
115
+ ```
116
+
117
+ ### Container Development
118
+
119
+ When developing in containers, use the official [`Dev Containers`](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension. This ensures Ruby LSP and Ruby LSP RSpec run inside the container, allowing correct spec path resolution.
120
+
121
+ For detailed container setup instructions, see the [Ruby LSP documentation](https://github.com/Shopify/ruby-lsp/blob/main/vscode/README.md?tab=readme-ov-file#developing-on-containers).
122
+
123
+ Make sure to configure Ruby LSP to run inside the container by adding it to your `.devcontainer.json`:
124
+
125
+ ```json
126
+ {
127
+ "name": "my-app",
128
+ // ...
129
+ "customizations": {
130
+ "vscode": {
131
+ "extensions": [
132
+ "Shopify.ruby-lsp",
133
+ // ...
134
+ ]
135
+ }
136
+ }
137
+ }
138
+ ```
139
+
59
140
  ## Development
60
141
 
61
142
  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.
@@ -1,7 +1,6 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "sorbet-runtime"
5
4
  require "ruby_lsp_rspec/version"
6
5
 
7
6
  module RubyLsp
@@ -8,69 +8,166 @@ require_relative "code_lens"
8
8
  require_relative "document_symbol"
9
9
  require_relative "definition"
10
10
  require_relative "indexing_enhancement"
11
+ require_relative "test_discovery"
12
+ require_relative "spec_style_patch"
11
13
 
12
14
  module RubyLsp
13
15
  module RSpec
14
16
  class Addon < ::RubyLsp::Addon
15
- extend T::Sig
17
+ FORMATTER_PATH = File.expand_path("rspec_formatter.rb", __dir__) #: String
18
+ FORMATTER_NAME = "RubyLsp::RSpec::RSpecFormatter" #: String
16
19
 
17
- sig { override.params(global_state: GlobalState, message_queue: Thread::Queue).void }
20
+ #: bool
21
+ attr_reader :debug
22
+
23
+ #: -> void
24
+ def initialize
25
+ super
26
+ @debug = false #: bool
27
+ @rspec_command = nil #: String?
28
+ end
29
+
30
+ # @override
31
+ #: (GlobalState, Thread::Queue) -> void
18
32
  def activate(global_state, message_queue)
19
- @index = T.let(global_state.index, T.nilable(RubyIndexer::Index))
33
+ @index = global_state.index #: RubyIndexer::Index?
34
+ @global_state = global_state #: GlobalState?
35
+
36
+ settings = global_state.settings_for_addon(name)
37
+ @rspec_command = rspec_command(settings)
38
+ @workspace_path = global_state.workspace_path #: String?
39
+ @debug = settings&.dig(:debug) || false
20
40
  end
21
41
 
22
- sig { override.void }
42
+ # @override
43
+ #: -> void
23
44
  def deactivate; end
24
45
 
25
- sig { override.returns(String) }
46
+ # @override
47
+ #: -> String
48
+ def name
49
+ "ruby-lsp-rspec"
50
+ end
51
+
52
+ # @override
53
+ #: -> String
26
54
  def version
27
55
  VERSION
28
56
  end
29
57
 
30
58
  # Creates a new CodeLens listener. This method is invoked on every CodeLens request
31
- sig do
32
- override.params(
33
- response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::CodeLens],
34
- uri: URI::Generic,
35
- dispatcher: Prism::Dispatcher,
36
- ).void
37
- end
59
+ # @override
60
+ #: (ResponseBuilders::CollectionResponseBuilder[Interface::CodeLens], URI::Generic, Prism::Dispatcher) -> void
38
61
  def create_code_lens_listener(response_builder, uri, dispatcher)
39
62
  return unless uri.to_standardized_path&.end_with?("_test.rb") || uri.to_standardized_path&.end_with?("_spec.rb")
63
+ return if @global_state&.enabled_feature?(:fullTestDiscovery)
40
64
 
41
- CodeLens.new(response_builder, uri, dispatcher)
65
+ CodeLens.new(
66
+ response_builder,
67
+ uri,
68
+ dispatcher,
69
+ @rspec_command, #: as !nil
70
+ debug: debug,
71
+ )
42
72
  end
43
73
 
44
- sig do
45
- override.params(
46
- response_builder: ResponseBuilders::DocumentSymbol,
47
- dispatcher: Prism::Dispatcher,
48
- ).void
74
+ # Creates a new Discover Tests listener. This method is invoked on every DiscoverTests request
75
+ # @override
76
+ #: (ResponseBuilders::TestCollection, Prism::Dispatcher, URI::Generic) -> void
77
+ def create_discover_tests_listener(response_builder, dispatcher, uri)
78
+ return unless uri.to_standardized_path&.end_with?("_spec.rb")
79
+
80
+ TestDiscovery.new(
81
+ response_builder,
82
+ dispatcher,
83
+ uri,
84
+ @workspace_path, #: as !nil
85
+ )
49
86
  end
87
+
88
+ # Resolves the minimal set of commands required to execute the requested tests
89
+ # @override
90
+ #: (Array[Hash[Symbol, untyped]]) -> Array[String]
91
+ def resolve_test_commands(items)
92
+ commands = []
93
+ queue = items.dup
94
+
95
+ full_files = []
96
+
97
+ until queue.empty?
98
+ item = queue.shift #: as !nil
99
+ tags = Set.new(item[:tags])
100
+ next unless tags.include?("framework:rspec")
101
+
102
+ children = item[:children]
103
+ uri = URI(item[:uri])
104
+ path = uri.full_path
105
+ next unless path
106
+
107
+ if tags.include?("test_dir")
108
+ if children.empty?
109
+ full_files.concat(Dir.glob(
110
+ "#{path}/**/*_spec.rb",
111
+ File::Constants::FNM_EXTGLOB | File::Constants::FNM_PATHNAME,
112
+ ))
113
+ end
114
+ elsif tags.include?("test_file")
115
+ full_files << path if children.empty?
116
+ elsif tags.include?("test_group")
117
+ start_line = item.dig(:range, :start, :line)
118
+ commands << "#{@rspec_command} -r #{FORMATTER_PATH} -f #{FORMATTER_NAME} #{path}:#{start_line + 1}"
119
+ else
120
+ full_files << "#{path}:#{item.dig(:range, :start, :line) + 1}"
121
+ end
122
+
123
+ queue.concat(children)
124
+ end
125
+
126
+ unless full_files.empty?
127
+ commands << "#{@rspec_command} -r #{FORMATTER_PATH} -f #{FORMATTER_NAME} #{full_files.join(" ")}"
128
+ end
129
+
130
+ commands
131
+ end
132
+
133
+ # @override
134
+ #: (ResponseBuilders::DocumentSymbol, Prism::Dispatcher) -> void
50
135
  def create_document_symbol_listener(response_builder, dispatcher)
51
136
  DocumentSymbol.new(response_builder, dispatcher)
52
137
  end
53
138
 
54
- sig do
55
- override.params(
56
- response_builder: ResponseBuilders::CollectionResponseBuilder[T.any(
57
- Interface::Location,
58
- Interface::LocationLink,
59
- )],
60
- uri: URI::Generic,
61
- node_context: NodeContext,
62
- dispatcher: Prism::Dispatcher,
63
- ).void
64
- end
139
+ # @override
140
+ #: (ResponseBuilders::CollectionResponseBuilder[Interface::Location | Interface::LocationLink], URI::Generic, NodeContext, Prism::Dispatcher) -> void
65
141
  def create_definition_listener(response_builder, uri, node_context, dispatcher)
66
142
  return unless uri.to_standardized_path&.end_with?("_test.rb") || uri.to_standardized_path&.end_with?("_spec.rb")
67
143
 
68
- Definition.new(response_builder, uri, node_context, T.must(@index), dispatcher)
144
+ Definition.new(
145
+ response_builder,
146
+ uri,
147
+ node_context,
148
+ @index, #: as !nil
149
+ dispatcher,
150
+ )
69
151
  end
70
152
 
71
- sig { override.returns(String) }
72
- def name
73
- "Ruby LSP RSpec"
153
+ private
154
+
155
+ #: (Hash[Symbol, untyped]?) -> String
156
+ def rspec_command(settings)
157
+ @rspec_command ||= settings&.dig(:rspecCommand) || begin
158
+ cmd = if File.exist?(File.join(Dir.pwd, "bin", "rspec"))
159
+ "bin/rspec"
160
+ else
161
+ "rspec"
162
+ end
163
+
164
+ begin
165
+ Bundler.with_original_env { Bundler.default_lockfile }
166
+ "bundle exec #{cmd}"
167
+ rescue Bundler::GemfileNotFound
168
+ cmd
169
+ end
170
+ end
74
171
  end
75
172
  end
76
173
  end
@@ -4,45 +4,24 @@
4
4
  module RubyLsp
5
5
  module RSpec
6
6
  class CodeLens
7
- extend T::Sig
8
-
9
7
  include ::RubyLsp::Requests::Support::Common
10
8
 
11
- sig do
12
- params(
13
- response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::CodeLens],
14
- uri: URI::Generic,
15
- dispatcher: Prism::Dispatcher,
16
- ).void
17
- end
18
- def initialize(response_builder, uri, dispatcher)
9
+ #: (ResponseBuilders::CollectionResponseBuilder[Interface::CodeLens], URI::Generic, Prism::Dispatcher, String, ?debug: bool) -> void
10
+ def initialize(response_builder, uri, dispatcher, rspec_command, debug: false)
19
11
  @response_builder = response_builder
20
12
  # Listener is only initialized if uri.to_standardized_path is valid
21
- @path = T.let(T.must(uri.to_standardized_path), String)
22
- @group_id = T.let(1, Integer)
23
- @group_id_stack = T.let([], T::Array[Integer])
24
- @anonymous_example_count = T.let(0, Integer)
13
+ path = uri.to_standardized_path #: as !nil
14
+ @path = path #: String
15
+ @group_id = 1 #: Integer
16
+ @group_id_stack = [] #: Array[Integer]
17
+ @rspec_command = rspec_command
18
+ @anonymous_example_count = 0 #: Integer
25
19
  dispatcher.register(self, :on_call_node_enter, :on_call_node_leave)
26
20
 
27
- @base_command = T.let(
28
- begin
29
- cmd = if File.exist?(File.join(Dir.pwd, "bin", "rspec"))
30
- "bin/rspec"
31
- else
32
- "rspec"
33
- end
34
-
35
- if File.exist?("Gemfile.lock")
36
- "bundle exec #{cmd}"
37
- else
38
- cmd
39
- end
40
- end,
41
- String,
42
- )
21
+ @debug = debug
43
22
  end
44
23
 
45
- sig { params(node: Prism::CallNode).void }
24
+ #: (Prism::CallNode) -> void
46
25
  def on_call_node_enter(node)
47
26
  case node.message
48
27
  when "example", "it", "specify"
@@ -59,7 +38,7 @@ module RubyLsp
59
38
  end
60
39
  end
61
40
 
62
- sig { params(node: Prism::CallNode).void }
41
+ #: (Prism::CallNode) -> void
63
42
  def on_call_node_leave(node)
64
43
  case node.message
65
44
  when "context", "describe"
@@ -71,12 +50,17 @@ module RubyLsp
71
50
 
72
51
  private
73
52
 
74
- sig { params(node: Prism::CallNode).returns(T::Boolean) }
53
+ #: (String) -> void
54
+ def log_message(message)
55
+ puts "[#{self.class}]: #{message}"
56
+ end
57
+
58
+ #: (Prism::CallNode) -> bool
75
59
  def valid_group?(node)
76
60
  !(node.block.nil? || (node.receiver && node.receiver&.slice != "RSpec"))
77
61
  end
78
62
 
79
- sig { params(node: Prism::CallNode).returns(String) }
63
+ #: (Prism::CallNode) -> String
80
64
  def generate_name(node)
81
65
  arguments = node.arguments&.arguments
82
66
 
@@ -99,10 +83,12 @@ module RubyLsp
99
83
  end
100
84
  end
101
85
 
102
- sig { params(node: Prism::Node, name: String, kind: Symbol).void }
86
+ #: (Prism::Node, name: String, kind: Symbol) -> void
103
87
  def add_test_code_lens(node, name:, kind:)
104
88
  line_number = node.location.start_line
105
- command = "#{@base_command} #{@path}:#{line_number}"
89
+ command = "#{@rspec_command} #{@path}:#{line_number}"
90
+
91
+ log_message("Full command: `#{command}`") if @debug
106
92
 
107
93
  grouping_data = { group_id: @group_id_stack.last, kind: kind }
108
94
  grouping_data[:id] = @group_id if kind == :group
@@ -4,22 +4,9 @@
4
4
  module RubyLsp
5
5
  module RSpec
6
6
  class Definition
7
- extend T::Sig
8
-
9
7
  include ::RubyLsp::Requests::Support::Common
10
8
 
11
- sig do
12
- params(
13
- response_builder: ResponseBuilders::CollectionResponseBuilder[T.any(
14
- Interface::Location,
15
- Interface::LocationLink,
16
- )],
17
- uri: URI::Generic,
18
- node_context: NodeContext,
19
- index: RubyIndexer::Index,
20
- dispatcher: Prism::Dispatcher,
21
- ).void
22
- end
9
+ #: (ResponseBuilders::CollectionResponseBuilder[Interface::LocationLink | Interface::Location], URI::Generic, NodeContext, RubyIndexer::Index, Prism::Dispatcher) -> void
23
10
  def initialize(response_builder, uri, node_context, index, dispatcher)
24
11
  @response_builder = response_builder
25
12
  @uri = uri
@@ -28,7 +15,7 @@ module RubyLsp
28
15
  dispatcher.register(self, :on_call_node_enter)
29
16
  end
30
17
 
31
- sig { params(node: Prism::CallNode).void }
18
+ #: (Prism::CallNode) -> void
32
19
  def on_call_node_enter(node)
33
20
  message = node.message
34
21
  return unless message
@@ -4,23 +4,16 @@
4
4
  module RubyLsp
5
5
  module RSpec
6
6
  class DocumentSymbol
7
- extend T::Sig
8
-
9
7
  include ::RubyLsp::Requests::Support::Common
10
8
 
11
- sig do
12
- params(
13
- response_builder: ResponseBuilders::DocumentSymbol,
14
- dispatcher: Prism::Dispatcher,
15
- ).void
16
- end
9
+ #: (ResponseBuilders::DocumentSymbol, Prism::Dispatcher) -> void
17
10
  def initialize(response_builder, dispatcher)
18
11
  @response_builder = response_builder
19
12
 
20
13
  dispatcher.register(self, :on_call_node_enter, :on_call_node_leave)
21
14
  end
22
15
 
23
- sig { params(node: Prism::CallNode).void }
16
+ #: (Prism::CallNode) -> void
24
17
  def on_call_node_enter(node)
25
18
  case node.message
26
19
  when "example", "it", "specify"
@@ -30,7 +23,7 @@ module RubyLsp
30
23
 
31
24
  @response_builder.last.children << RubyLsp::Interface::DocumentSymbol.new(
32
25
  name: name,
33
- kind: LanguageServer::Protocol::Constant::SymbolKind::METHOD,
26
+ kind: RubyLsp::Constant::SymbolKind::METHOD,
34
27
  selection_range: range_from_node(node),
35
28
  range: range_from_node(node),
36
29
  )
@@ -43,7 +36,7 @@ module RubyLsp
43
36
 
44
37
  symbol = RubyLsp::Interface::DocumentSymbol.new(
45
38
  name: name,
46
- kind: LanguageServer::Protocol::Constant::SymbolKind::MODULE,
39
+ kind: RubyLsp::Constant::SymbolKind::MODULE,
47
40
  selection_range: range_from_node(node),
48
41
  range: range_from_node(node),
49
42
  children: [],
@@ -54,7 +47,7 @@ module RubyLsp
54
47
  end
55
48
  end
56
49
 
57
- sig { params(node: Prism::CallNode).void }
50
+ #: (Prism::CallNode) -> void
58
51
  def on_call_node_leave(node)
59
52
  case node.message
60
53
  when "context", "describe", "shared_examples", "shared_context", "shared_examples_for"
@@ -64,7 +57,7 @@ module RubyLsp
64
57
  end
65
58
  end
66
59
 
67
- sig { params(node: Prism::CallNode).returns(T.nilable(String)) }
60
+ #: (Prism::CallNode) -> String?
68
61
  def generate_name(node)
69
62
  arguments = node.arguments&.arguments
70
63
 
@@ -4,9 +4,8 @@
4
4
  module RubyLsp
5
5
  module RSpec
6
6
  class IndexingEnhancement < RubyIndexer::Enhancement
7
- extend T::Sig
8
-
9
- sig { override.params(node: Prism::CallNode).void }
7
+ # @override
8
+ #: (Prism::CallNode) -> void
10
9
  def on_call_node_enter(node)
11
10
  return if node.receiver
12
11
 
@@ -22,7 +21,7 @@ module RubyLsp
22
21
 
23
22
  return if arguments.arguments.count != 1
24
23
 
25
- method_name_node = T.must(arguments.arguments.first)
24
+ method_name_node = arguments.arguments.first #: as !nil
26
25
 
27
26
  method_name = case method_name_node
28
27
  when Prism::StringNode
@@ -41,7 +40,7 @@ module RubyLsp
41
40
  arguments = node.arguments
42
41
 
43
42
  if arguments && arguments.arguments.count == 1
44
- method_name_node = T.must(arguments.arguments.first)
43
+ method_name_node = arguments.arguments.first #: as !nil
45
44
  end
46
45
 
47
46
  method_name = if method_name_node
@@ -0,0 +1,66 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "rspec/core/formatters"
5
+ require "ruby_lsp/test_reporters/lsp_reporter"
6
+
7
+ module RubyLsp
8
+ module RSpec
9
+ class RSpecFormatter
10
+ ::RSpec::Core::Formatters.register(
11
+ self,
12
+ :example_passed,
13
+ :example_pending,
14
+ :example_failed,
15
+ :example_started,
16
+ :stop,
17
+ )
18
+
19
+ def initialize(output)
20
+ @output = output
21
+ end
22
+
23
+ def example_started(notification)
24
+ example = notification.example
25
+ uri = uri_for(example)
26
+ id = generate_id(example)
27
+ line = example.location.split(":").last
28
+ RubyLsp::LspReporter.instance.start_test(id: id, uri: uri, line: line)
29
+ end
30
+
31
+ def example_passed(notification)
32
+ example = notification.example
33
+ uri = uri_for(example)
34
+ id = generate_id(example)
35
+ RubyLsp::LspReporter.instance.record_pass(id: id, uri: uri)
36
+ end
37
+
38
+ def example_failed(notification)
39
+ example = notification.example
40
+ uri = uri_for(example)
41
+ id = generate_id(example)
42
+ RubyLsp::LspReporter.instance.record_fail(id: id, message: notification.exception.message, uri: uri)
43
+ end
44
+
45
+ def example_pending(notification)
46
+ example = notification.example
47
+ uri = uri_for(example)
48
+ id = generate_id(example)
49
+ RubyLsp::LspReporter.instance.record_skip(id: id, uri: uri)
50
+ end
51
+
52
+ def stop(notification)
53
+ RubyLsp::LspReporter.instance.shutdown
54
+ end
55
+
56
+ def uri_for(example)
57
+ absolute_path = File.expand_path(example.file_path)
58
+ URI::Generic.from_path(path: absolute_path)
59
+ end
60
+
61
+ def generate_id(example)
62
+ [example, *example.example_group.parent_groups].reverse.map(&:location).join("::")
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,14 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ module Listeners
6
+ # Patching this listener so it doesn't generate test items for RSpec tests
7
+ class SpecStyle
8
+ #: (ResponseBuilders::TestCollection, GlobalState, Prism::Dispatcher, URI::Generic) -> void
9
+ def initialize(response_builder, global_state, dispatcher, uri)
10
+ super
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,131 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ module RSpec
6
+ class TestDiscovery
7
+ include ::RubyLsp::Requests::Support::Common
8
+
9
+ #: (ResponseBuilders::TestCollection, Prism::Dispatcher, URI::Generic, String) -> void
10
+ def initialize(response_builder, dispatcher, uri, workspace_path)
11
+ @response_builder = response_builder
12
+ @dispatcher = dispatcher
13
+ @uri = uri
14
+
15
+ path = uri.to_standardized_path #: as !nil
16
+ @path = path #: String
17
+ @workspace_path = workspace_path #: String
18
+ @group_stack = [] #: Array[::RubyLsp::Requests::Support::TestItem]
19
+
20
+ dispatcher.register(
21
+ self,
22
+ :on_call_node_enter,
23
+ :on_call_node_leave,
24
+ )
25
+ end
26
+
27
+ #: (Prism::CallNode) -> void
28
+ def on_call_node_enter(node)
29
+ return unless ["describe", "context", "it", "specify", "example"].include?(node.message)
30
+
31
+ case node.message
32
+ when "describe", "context"
33
+ handle_describe(node)
34
+ when "it", "specify", "example"
35
+ handle_example(node)
36
+ end
37
+ end
38
+
39
+ #: (Prism::CallNode) -> void
40
+ def on_call_node_leave(node)
41
+ case node.message
42
+ when "context", "describe"
43
+ return unless valid_group?(node)
44
+
45
+ @group_stack.pop
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ #: (Prism::CallNode) -> String?
52
+ def extract_description(node)
53
+ # Try to extract the description from a string literal argument
54
+ first_arg = node.arguments&.arguments&.first
55
+ return "example at #{relative_location(node)}" if first_arg.nil?
56
+
57
+ case first_arg
58
+ when Prism::StringNode
59
+ first_arg.content
60
+ when Prism::SymbolNode
61
+ first_arg.value
62
+ when Prism::ConstantReadNode
63
+ first_arg.name.to_s
64
+ when Prism::ConstantPathNode
65
+ first_arg.full_name
66
+ end
67
+ end
68
+
69
+ #: (Prism::CallNode) -> void
70
+ def handle_describe(node)
71
+ description = extract_description(node)
72
+ return if description.nil?
73
+
74
+ parent = find_parent_test_group
75
+ parent_id = parent ? "#{parent.id}::" : ""
76
+
77
+ test_item = ::RubyLsp::Requests::Support::TestItem.new(
78
+ "#{parent_id}#{relative_location(node)}",
79
+ description,
80
+ @uri,
81
+ range_from_node(node),
82
+ framework: :rspec,
83
+ )
84
+
85
+ if parent
86
+ parent.add(test_item)
87
+ else
88
+ @response_builder.add(test_item)
89
+ end
90
+
91
+ @response_builder.add_code_lens(test_item)
92
+ @group_stack.push(test_item)
93
+ end
94
+
95
+ #: (Prism::CallNode) -> void
96
+ def handle_example(node)
97
+ description = extract_description(node)
98
+ parent = find_parent_test_group
99
+ return unless parent
100
+
101
+ test_item = ::RubyLsp::Requests::Support::TestItem.new(
102
+ "#{parent.id}::#{relative_location(node)}",
103
+ description,
104
+ @uri,
105
+ range_from_node(node),
106
+ framework: :rspec,
107
+ )
108
+
109
+ parent.add(test_item)
110
+ @response_builder.add_code_lens(test_item)
111
+ end
112
+
113
+ #: -> ::RubyLsp::Requests::Support::TestItem??
114
+ def find_parent_test_group
115
+ @group_stack.last
116
+ end
117
+
118
+ #: (Prism::CallNode) -> bool
119
+ def valid_group?(node)
120
+ !(node.block.nil? || (node.receiver && node.receiver&.slice != "RSpec"))
121
+ end
122
+
123
+ #: (Prism::CallNode) -> String
124
+ def relative_location(node)
125
+ uri_path = @uri.to_standardized_path #: as !nil
126
+ relative_path = Pathname.new(uri_path).relative_path_from(Pathname.new(@workspace_path))
127
+ "./#{relative_path}:#{node.location.start_line}"
128
+ end
129
+ end
130
+ end
131
+ end
@@ -3,6 +3,6 @@
3
3
 
4
4
  module RubyLsp
5
5
  module RSpec
6
- VERSION = "0.1.21"
6
+ VERSION = "0.1.23"
7
7
  end
8
8
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-lsp-rspec
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.21
4
+ version: 0.1.23
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stan Lo
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2025-01-25 00:00:00.000000000 Z
10
+ date: 2025-05-14 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: ruby-lsp
@@ -16,14 +15,14 @@ dependencies:
16
15
  requirements:
17
16
  - - "~>"
18
17
  - !ruby/object:Gem::Version
19
- version: 0.23.0
18
+ version: 0.23.19
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
23
  - - "~>"
25
24
  - !ruby/object:Gem::Version
26
- version: 0.23.0
25
+ version: 0.23.19
27
26
  description: RSpec addon for ruby-lsp
28
27
  email:
29
28
  - stan001212@gmail.com
@@ -44,6 +43,9 @@ files:
44
43
  - lib/ruby_lsp/ruby_lsp_rspec/definition.rb
45
44
  - lib/ruby_lsp/ruby_lsp_rspec/document_symbol.rb
46
45
  - lib/ruby_lsp/ruby_lsp_rspec/indexing_enhancement.rb
46
+ - lib/ruby_lsp/ruby_lsp_rspec/rspec_formatter.rb
47
+ - lib/ruby_lsp/ruby_lsp_rspec/spec_style_patch.rb
48
+ - lib/ruby_lsp/ruby_lsp_rspec/test_discovery.rb
47
49
  - lib/ruby_lsp_rspec/version.rb
48
50
  homepage: https://github.com/st0012/ruby-lsp-rspec
49
51
  licenses:
@@ -52,7 +54,6 @@ metadata:
52
54
  homepage_uri: https://github.com/st0012/ruby-lsp-rspec
53
55
  source_code_uri: https://github.com/st0012/ruby-lsp-rspec
54
56
  changelog_uri: https://github.com/st0012/ruby-lsp-rspec/releases
55
- post_install_message:
56
57
  rdoc_options: []
57
58
  require_paths:
58
59
  - lib
@@ -67,8 +68,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
67
68
  - !ruby/object:Gem::Version
68
69
  version: '0'
69
70
  requirements: []
70
- rubygems_version: 3.5.11
71
- signing_key:
71
+ rubygems_version: 3.6.3
72
72
  specification_version: 4
73
73
  summary: RSpec addon for ruby-lsp
74
74
  test_files: []