ruby-lsp 0.5.1 → 0.6.1

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: eadf22b9ba9191df338c0da93b3ce6b98ddf61a4831cf204f79ad7c636695fcd
4
- data.tar.gz: c0f7fc6ec73638ffc2075e86e40925a14a2e629d9ee79e10a0ed5039f9fee3f2
3
+ metadata.gz: 11dd3401d408ed456da9b4dd0aca906b991a45a0103ca8786eafbf34d4659c85
4
+ data.tar.gz: 8469dd62a3660965daee589835e1481fe09e17b55cbd9d61c461d4ba94db833e
5
5
  SHA512:
6
- metadata.gz: b3ceb8e427dabbb4819d105b29a82fb719c61c4d2bd69b48c215b7010b6ad79f06bf71f57dc936f8ef74440fd0678b5e3640880008e8d76fe3be50091b8e0da1
7
- data.tar.gz: 855d9cff0ee7a3db7fe2699b08fe8d3cb7fc7edb39448fb9821ab58b4c234763f2489388a995de216e90f05ecbc4e4b22b7ef23b266bdff9654efbc0a1dc6bcd
6
+ metadata.gz: 6d2e0c3d9c5bf11068b1d5e3092d4b5f3b233066a33b7e74ea218f115fbb8b154798a8b8b0634569781245f1dedcb6da3e5ca3a23db04f78fc105c496d2a3280
7
+ data.tar.gz: 529ce841833605ac40c67536bbca1eb1a9568b742de854f4698ced147813ec63569fd926d4035d518f7bb0ab54d2bbd4665e7ad2564923b49036573a0d4625e7
data/README.md CHANGED
@@ -39,10 +39,14 @@ end
39
39
  See the [documentation](https://shopify.github.io/ruby-lsp) for more in-depth details about the
40
40
  [supported features](https://shopify.github.io/ruby-lsp/RubyLsp/Requests.html).
41
41
 
42
+ For creating rich themes for Ruby using the semantic highlighting information, see the [semantic highlighting
43
+ documentation](SEMANTIC_HIGHLIGHTING.md).
44
+
42
45
  ## Learn More
43
46
 
44
47
  * [RubyConf 2022: Improving the development experience with language servers](https://www.youtube.com/watch?v=kEfXPTm1aCI) ([Vinicius Stock](https://github.com/vinistock))
45
48
  * [Remote Ruby: Ruby Language Server with Vinicius Stock](https://remoteruby.com/221)
49
+ * [RubyKaigi 2023: Code indexing - How language servers understand our code](https://www.youtube.com/watch?v=ks3tQojSJLU) ([Vinicius Stock](https://github.com/vinistock))
46
50
 
47
51
  ## Contributing
48
52
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.5.1
1
+ 0.6.1
data/exe/ruby-lsp CHANGED
@@ -20,7 +20,39 @@ end
20
20
 
21
21
  require_relative "../lib/ruby_lsp/internal"
22
22
 
23
- if ARGV.include?("--debug")
23
+ require "optparse"
24
+
25
+ options = {}
26
+ parser = OptionParser.new do |opts|
27
+ opts.banner = "Usage: ruby-lsp [options]"
28
+
29
+ opts.on("--version", "Print ruby-lsp version") do
30
+ puts RubyLsp::VERSION
31
+ exit(0)
32
+ end
33
+
34
+ opts.on("--debug", "Launch the Ruby LSP with a debugger attached") do
35
+ options[:debug] = true
36
+ end
37
+
38
+ opts.on("-h", "--help", "Print this help") do
39
+ puts opts.help
40
+ puts
41
+ puts "See https://shopify.github.io/ruby-lsp/ for more information"
42
+ exit(0)
43
+ end
44
+ end
45
+
46
+ begin
47
+ parser.parse!
48
+ rescue OptionParser::InvalidOption => e
49
+ warn(e)
50
+ warn("")
51
+ warn(parser.help)
52
+ exit(1)
53
+ end
54
+
55
+ if options[:debug]
24
56
  if ["x64-mingw-ucrt", "x64-mingw32"].include?(RUBY_PLATFORM)
25
57
  puts "Debugging is not supported on Windows"
26
58
  exit 1
@@ -6,23 +6,26 @@ require "objspace"
6
6
 
7
7
  module RubyLsp
8
8
  # This rake task checks that all requests or extensions are fully documented. Add the rake task to your Rakefile and
9
- # specify the absolute path for all files that must be required in order to discover all listeners
9
+ # specify the absolute path for all files that must be required in order to discover all listeners and their
10
+ # related GIFs
10
11
  #
11
12
  # # Rakefile
12
13
  # request_files = FileList.new("#{__dir__}/lib/ruby_lsp/requests/*.rb") do |fl|
13
14
  # fl.exclude(/base_request\.rb/)
14
15
  # end
15
- # RubyLsp::CheckDocs.new(request_files)
16
+ # gif_files = FileList.new("#{__dir__}/**/*.gif")
17
+ # RubyLsp::CheckDocs.new(request_files, gif_files)
16
18
  # # Run with bundle exec rake ruby_lsp:check_docs
17
19
  class CheckDocs < Rake::TaskLib
18
20
  extend T::Sig
19
21
 
20
- sig { params(require_files: Rake::FileList).void }
21
- def initialize(require_files)
22
+ sig { params(require_files: Rake::FileList, gif_files: Rake::FileList).void }
23
+ def initialize(require_files, gif_files)
22
24
  super()
23
25
 
24
26
  @name = T.let("ruby_lsp:check_docs", String)
25
27
  @file_list = require_files
28
+ @gif_list = gif_files
26
29
  define_task
27
30
  end
28
31
 
@@ -34,6 +37,13 @@ module RubyLsp
34
37
  task(@name) { run_task }
35
38
  end
36
39
 
40
+ sig { params(request_path: String).returns(T::Boolean) }
41
+ def gif_exists?(request_path)
42
+ request_gif = request_path.gsub(".rb", ".gif").split("/").last
43
+
44
+ @gif_list.any? { |gif_path| gif_path.end_with?(request_gif) }
45
+ end
46
+
37
47
  sig { void }
38
48
  def run_task
39
49
  # Require all files configured to make sure all listeners are loaded
@@ -42,7 +52,7 @@ module RubyLsp
42
52
  # Find all classes that inherit from BaseRequest or Listener, which are the ones we want to make sure are
43
53
  # documented
44
54
  features = ObjectSpace.each_object(Class).filter_map do |k|
45
- klass = T.cast(k, Class)
55
+ klass = T.cast(k, T::Class[T.anything])
46
56
  klass if klass < RubyLsp::Requests::BaseRequest || klass < RubyLsp::Listener
47
57
  end
48
58
 
@@ -93,6 +103,14 @@ module RubyLsp
93
103
 
94
104
  # [Inlay hint demo](../../inlay_hint.gif)
95
105
  DOCS
106
+ elsif !gif_exists?(file_path)
107
+ T.must(missing_docs[class_name]) << <<~DOCS
108
+ The GIF for the request documentation does not exist. Make sure to add it,
109
+ with the same naming as the request. For example:
110
+
111
+ # lib/ruby_lsp/requests/code_lens.rb
112
+ # foo/bar/code_lens.gif
113
+ DOCS
96
114
  end
97
115
  end
98
116
 
@@ -20,6 +20,7 @@ module RubyLsp
20
20
  # ```
21
21
  class EventEmitter < SyntaxTree::Visitor
22
22
  extend T::Sig
23
+ include SyntaxTree::WithScope
23
24
 
24
25
  sig { void }
25
26
  def initialize
@@ -73,6 +74,12 @@ module RubyLsp
73
74
  @listeners[:after_command]&.each { |l| T.unsafe(l).after_command(node) }
74
75
  end
75
76
 
77
+ sig { override.params(node: SyntaxTree::CommandCall).void }
78
+ def visit_command_call(node)
79
+ @listeners[:on_command_call]&.each { |l| T.unsafe(l).on_command_call(node) }
80
+ super
81
+ end
82
+
76
83
  sig { override.params(node: SyntaxTree::CallNode).void }
77
84
  def visit_call(node)
78
85
  @listeners[:on_call]&.each { |l| T.unsafe(l).on_call(node) }
@@ -116,5 +123,59 @@ module RubyLsp
116
123
  @listeners[:on_comment]&.each { |l| T.unsafe(l).on_comment(node) }
117
124
  super
118
125
  end
126
+
127
+ sig { override.params(node: SyntaxTree::Rescue).void }
128
+ def visit_rescue(node)
129
+ @listeners[:on_rescue]&.each { |l| T.unsafe(l).on_rescue(node) }
130
+ super
131
+ end
132
+
133
+ sig { override.params(node: SyntaxTree::Kw).void }
134
+ def visit_kw(node)
135
+ @listeners[:on_kw]&.each { |l| T.unsafe(l).on_kw(node) }
136
+ super
137
+ end
138
+
139
+ sig { override.params(node: SyntaxTree::Params).void }
140
+ def visit_params(node)
141
+ @listeners[:on_params]&.each { |l| T.unsafe(l).on_params(node) }
142
+ super
143
+ end
144
+
145
+ sig { override.params(node: SyntaxTree::Field).void }
146
+ def visit_field(node)
147
+ @listeners[:on_field]&.each { |l| T.unsafe(l).on_field(node) }
148
+ super
149
+ end
150
+
151
+ sig { override.params(node: SyntaxTree::VarRef).void }
152
+ def visit_var_ref(node)
153
+ @listeners[:on_var_ref]&.each { |l| T.unsafe(l).on_var_ref(node) }
154
+ super
155
+ end
156
+
157
+ sig { override.params(node: SyntaxTree::BlockVar).void }
158
+ def visit_block_var(node)
159
+ @listeners[:on_block_var]&.each { |l| T.unsafe(l).on_block_var(node) }
160
+ super
161
+ end
162
+
163
+ sig { override.params(node: SyntaxTree::LambdaVar).void }
164
+ def visit_lambda_var(node)
165
+ @listeners[:on_lambda_var]&.each { |l| T.unsafe(l).on_lambda_var(node) }
166
+ super
167
+ end
168
+
169
+ sig { override.params(node: SyntaxTree::Binary).void }
170
+ def visit_binary(node)
171
+ super
172
+ @listeners[:after_binary]&.each { |l| T.unsafe(l).after_binary(node) }
173
+ end
174
+
175
+ sig { override.params(node: SyntaxTree::Const).void }
176
+ def visit_const(node)
177
+ @listeners[:on_const]&.each { |l| T.unsafe(l).on_const(node) }
178
+ super
179
+ end
119
180
  end
120
181
  end
@@ -1,6 +1,8 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "ruby_lsp/requests/support/dependency_detector"
5
+
4
6
  module RubyLsp
5
7
  # This class dispatches a request execution to the right request class. No IO should happen anywhere here!
6
8
  class Executor
@@ -11,6 +13,7 @@ module RubyLsp
11
13
  # Requests that mutate the store must be run sequentially! Parallel requests only receive a temporary copy of the
12
14
  # store
13
15
  @store = store
16
+ @test_library = T.let(DependencyDetector.detected_test_library, String)
14
17
  @message_queue = message_queue
15
18
  end
16
19
 
@@ -81,7 +84,8 @@ module RubyLsp
81
84
  folding_range(uri)
82
85
  when "textDocument/selectionRange"
83
86
  selection_range(uri, request.dig(:params, :positions))
84
- when "textDocument/documentSymbol", "textDocument/documentLink", "textDocument/codeLens"
87
+ when "textDocument/documentSymbol", "textDocument/documentLink", "textDocument/codeLens",
88
+ "textDocument/semanticTokens/full"
85
89
  document = @store.get(uri)
86
90
 
87
91
  # If the response has already been cached by another request, return it
@@ -92,10 +96,11 @@ module RubyLsp
92
96
  emitter = EventEmitter.new
93
97
  document_symbol = Requests::DocumentSymbol.new(emitter, @message_queue)
94
98
  document_link = Requests::DocumentLink.new(uri, emitter, @message_queue)
95
- code_lens = Requests::CodeLens.new(uri, emitter, @message_queue)
99
+ code_lens = Requests::CodeLens.new(uri, emitter, @message_queue, @test_library)
96
100
  code_lens_extensions_listeners = Requests::CodeLens.listeners.map do |l|
97
101
  T.unsafe(l).new(document.uri, emitter, @message_queue)
98
102
  end
103
+ semantic_highlighting = Requests::SemanticHighlighting.new(emitter, @message_queue)
99
104
  emitter.visit(document.tree) if document.parsed?
100
105
 
101
106
  code_lens_extensions_listeners.each { |ext| code_lens.merge_response!(ext) }
@@ -105,9 +110,11 @@ module RubyLsp
105
110
  document.cache_set("textDocument/documentSymbol", document_symbol.response)
106
111
  document.cache_set("textDocument/documentLink", document_link.response)
107
112
  document.cache_set("textDocument/codeLens", code_lens.response)
113
+ document.cache_set(
114
+ "textDocument/semanticTokens/full",
115
+ Requests::Support::SemanticTokenEncoder.new.encode(semantic_highlighting.response),
116
+ )
108
117
  document.cache_get(request[:method])
109
- when "textDocument/semanticTokens/full"
110
- semantic_tokens_full(uri)
111
118
  when "textDocument/semanticTokens/range"
112
119
  semantic_tokens_range(uri, request.dig(:params, :range))
113
120
  when "textDocument/formatting"
@@ -245,19 +252,6 @@ module RubyLsp
245
252
  end
246
253
  end
247
254
 
248
- sig { params(uri: String).returns(Interface::SemanticTokens) }
249
- def semantic_tokens_full(uri)
250
- @store.cache_fetch(uri, "textDocument/semanticTokens/full") do |document|
251
- T.cast(
252
- Requests::SemanticHighlighting.new(
253
- document,
254
- encoder: Requests::Support::SemanticTokenEncoder.new,
255
- ).run,
256
- Interface::SemanticTokens,
257
- )
258
- end
259
- end
260
-
261
255
  sig { params(uri: String).returns(T.nilable(T::Array[Interface::TextEdit])) }
262
256
  def formatting(uri)
263
257
  # If formatter is set to `auto` but no supported formatting gem is found, don't attempt to format
@@ -287,13 +281,18 @@ module RubyLsp
287
281
  Requests::DocumentHighlight.new(@store.get(uri), position).run
288
282
  end
289
283
 
290
- sig { params(uri: String, range: Document::RangeShape).returns(T::Array[Interface::InlayHint]) }
284
+ sig { params(uri: String, range: Document::RangeShape).returns(T.nilable(T::Array[Interface::InlayHint])) }
291
285
  def inlay_hint(uri, range)
292
286
  document = @store.get(uri)
287
+ return if document.syntax_error?
288
+
293
289
  start_line = range.dig(:start, :line)
294
290
  end_line = range.dig(:end, :line)
295
291
 
296
- Requests::InlayHints.new(document, start_line..end_line).run
292
+ emitter = EventEmitter.new
293
+ listener = Requests::InlayHints.new(start_line..end_line, emitter, @message_queue)
294
+ emitter.visit(document.tree)
295
+ listener.response
297
296
  end
298
297
 
299
298
  sig do
@@ -354,14 +353,15 @@ module RubyLsp
354
353
  start_line = range.dig(:start, :line)
355
354
  end_line = range.dig(:end, :line)
356
355
 
357
- T.cast(
358
- Requests::SemanticHighlighting.new(
359
- document,
360
- range: start_line..end_line,
361
- encoder: Requests::Support::SemanticTokenEncoder.new,
362
- ).run,
363
- Interface::SemanticTokens,
356
+ emitter = EventEmitter.new
357
+ listener = Requests::SemanticHighlighting.new(
358
+ emitter,
359
+ @message_queue,
360
+ range: start_line..end_line,
364
361
  )
362
+ emitter.visit(document.tree) if document.parsed?
363
+
364
+ Requests::Support::SemanticTokenEncoder.new.encode(listener.response)
365
365
  end
366
366
 
367
367
  sig do
@@ -421,9 +421,9 @@ module RubyLsp
421
421
  encodings.first
422
422
  end
423
423
 
424
- formatter = options.dig(:initializationOptions, :formatter)
424
+ formatter = options.dig(:initializationOptions, :formatter) || "auto"
425
425
  @store.formatter = if formatter == "auto"
426
- detected_formatter
426
+ DependencyDetector.detected_formatter
427
427
  else
428
428
  formatter
429
429
  end
@@ -536,23 +536,6 @@ module RubyLsp
536
536
  )
537
537
  end
538
538
 
539
- sig { returns(String) }
540
- def detected_formatter
541
- # NOTE: Intentionally no $ at end, since we want to match rubocop-shopify, etc.
542
- if direct_dependency?(/^rubocop/)
543
- "rubocop"
544
- elsif direct_dependency?(/^syntax_tree$/)
545
- "syntax_tree"
546
- else
547
- "none"
548
- end
549
- end
550
-
551
- sig { params(gem_pattern: Regexp).returns(T::Boolean) }
552
- def direct_dependency?(gem_pattern)
553
- Bundler.locked_gems.dependencies.keys.grep(gem_pattern).any?
554
- end
555
-
556
539
  sig { void }
557
540
  def check_formatter_is_available
558
541
  # Warn of an unavailable `formatter` setting, e.g. `rubocop` on a project which doesn't have RuboCop.
@@ -560,11 +543,13 @@ module RubyLsp
560
543
  return unless @store.formatter == "rubocop"
561
544
 
562
545
  unless defined?(RubyLsp::Requests::Support::RuboCopRunner)
546
+ @store.formatter = "none"
547
+
563
548
  @message_queue << Notification.new(
564
549
  message: "window/showMessage",
565
550
  params: Interface::ShowMessageParams.new(
566
551
  type: Constant::MessageType::ERROR,
567
- message: "Ruby LSP formatter is set to `rubocop` but RuboCop was not found in the bundle.",
552
+ message: "Ruby LSP formatter is set to `rubocop` but RuboCop was not found in the Gemfile or gemspec.",
568
553
  ),
569
554
  )
570
555
  end
@@ -11,12 +11,8 @@ module RubyLsp
11
11
 
12
12
  abstract!
13
13
 
14
- # We must accept rest keyword arguments here, so that the argument count matches when
15
- # SyntaxTree::WithScope#initialize invokes `super` for Sorbet. We don't actually use these parameters for
16
- # anything. We can remove these arguments once we drop support for Ruby 2.7
17
- # https://github.com/ruby-syntax-tree/syntax_tree/blob/4dac90b53df388f726dce50ce638a1ba71cc59f8/lib/syntax_tree/with_scope.rb#L122
18
- sig { params(document: Document, _kwargs: T.untyped).void }
19
- def initialize(document, **_kwargs)
14
+ sig { params(document: Document).void }
15
+ def initialize(document)
20
16
  @document = document
21
17
  super()
22
18
  end
@@ -1,6 +1,8 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "shellwords"
5
+
4
6
  module RubyLsp
5
7
  module Requests
6
8
  # ![Code lens demo](../../code_lens.gif)
@@ -31,35 +33,65 @@ module RubyLsp
31
33
  sig { override.returns(ResponseType) }
32
34
  attr_reader :response
33
35
 
34
- sig { params(uri: String, emitter: EventEmitter, message_queue: Thread::Queue).void }
35
- def initialize(uri, emitter, message_queue)
36
+ sig { params(uri: String, emitter: EventEmitter, message_queue: Thread::Queue, test_library: String).void }
37
+ def initialize(uri, emitter, message_queue, test_library)
36
38
  super(emitter, message_queue)
37
39
 
40
+ @test_library = T.let(test_library, String)
38
41
  @response = T.let([], ResponseType)
39
42
  @path = T.let(T.must(URI(uri).path), String)
40
- @visibility = T.let("public", String)
41
- @prev_visibility = T.let("public", String)
42
-
43
- emitter.register(self, :on_class, :on_def, :on_command, :after_command, :on_call, :after_call, :on_vcall)
43
+ # visibility_stack is a stack of [current_visibility, previous_visibility]
44
+ @visibility_stack = T.let([["public", "public"]], T::Array[T::Array[T.nilable(String)]])
45
+ @class_stack = T.let([], T::Array[String])
46
+
47
+ emitter.register(
48
+ self,
49
+ :on_class,
50
+ :after_class,
51
+ :on_def,
52
+ :on_command,
53
+ :after_command,
54
+ :on_call,
55
+ :after_call,
56
+ :on_vcall,
57
+ )
44
58
  end
45
59
 
46
60
  sig { params(node: SyntaxTree::ClassDeclaration).void }
47
61
  def on_class(node)
62
+ @visibility_stack.push(["public", "public"])
48
63
  class_name = node.constant.constant.value
49
64
  if class_name.end_with?("Test")
50
- add_code_lens(node, name: class_name, command: BASE_COMMAND + @path)
65
+ @class_stack.push(class_name)
66
+ add_test_code_lens(
67
+ node,
68
+ name: class_name,
69
+ command: generate_test_command(class_name: class_name),
70
+ kind: :group,
71
+ )
51
72
  end
52
73
  end
53
74
 
75
+ sig { params(node: SyntaxTree::ClassDeclaration).void }
76
+ def after_class(node)
77
+ @visibility_stack.pop
78
+ @class_stack.pop
79
+ end
80
+
54
81
  sig { params(node: SyntaxTree::DefNode).void }
55
82
  def on_def(node)
56
- if @visibility == "public"
83
+ class_name = @class_stack.last
84
+ return unless class_name
85
+
86
+ visibility, _ = @visibility_stack.last
87
+ if visibility == "public"
57
88
  method_name = node.name.value
58
89
  if method_name.start_with?("test_")
59
- add_code_lens(
90
+ add_test_code_lens(
60
91
  node,
61
92
  name: method_name,
62
- command: BASE_COMMAND + @path + " --name " + method_name,
93
+ command: generate_test_command(method_name: method_name, class_name: class_name),
94
+ kind: :example,
63
95
  )
64
96
  end
65
97
  end
@@ -67,15 +99,22 @@ module RubyLsp
67
99
 
68
100
  sig { params(node: SyntaxTree::Command).void }
69
101
  def on_command(node)
70
- if ACCESS_MODIFIERS.include?(node.message.value) && node.arguments.parts.any?
71
- @prev_visibility = @visibility
72
- @visibility = node.message.value
102
+ node_message = node.message.value
103
+ if ACCESS_MODIFIERS.include?(node_message) && node.arguments.parts.any?
104
+ visibility, _ = @visibility_stack.pop
105
+ @visibility_stack.push([node_message, visibility])
106
+ elsif @path.include?("Gemfile") && node_message.include?("gem") && node.arguments.parts.any?
107
+ remote = resolve_gem_remote(node)
108
+ return unless remote
109
+
110
+ add_open_gem_remote_code_lens(node, remote)
73
111
  end
74
112
  end
75
113
 
76
114
  sig { params(node: SyntaxTree::Command).void }
77
115
  def after_command(node)
78
- @visibility = @prev_visibility
116
+ _, prev_visibility = @visibility_stack.pop
117
+ @visibility_stack.push([prev_visibility, prev_visibility])
79
118
  end
80
119
 
81
120
  sig { params(node: SyntaxTree::CallNode).void }
@@ -85,15 +124,16 @@ module RubyLsp
85
124
  if ident
86
125
  ident_value = T.cast(ident, SyntaxTree::Ident).value
87
126
  if ACCESS_MODIFIERS.include?(ident_value)
88
- @prev_visibility = @visibility
89
- @visibility = ident_value
127
+ visibility, _ = @visibility_stack.pop
128
+ @visibility_stack.push([ident_value, visibility])
90
129
  end
91
130
  end
92
131
  end
93
132
 
94
133
  sig { params(node: SyntaxTree::CallNode).void }
95
134
  def after_call(node)
96
- @visibility = @prev_visibility
135
+ _, prev_visibility = @visibility_stack.pop
136
+ @visibility_stack.push([prev_visibility, prev_visibility])
97
137
  end
98
138
 
99
139
  sig { params(node: SyntaxTree::VCall).void }
@@ -101,8 +141,8 @@ module RubyLsp
101
141
  vcall_value = node.value.value
102
142
 
103
143
  if ACCESS_MODIFIERS.include?(vcall_value)
104
- @prev_visibility = vcall_value
105
- @visibility = vcall_value
144
+ @visibility_stack.pop
145
+ @visibility_stack.push([vcall_value, vcall_value])
106
146
  end
107
147
  end
108
148
 
@@ -114,36 +154,88 @@ module RubyLsp
114
154
 
115
155
  private
116
156
 
117
- sig { params(node: SyntaxTree::Node, name: String, command: String).void }
118
- def add_code_lens(node, name:, command:)
157
+ sig { params(node: SyntaxTree::Node, name: String, command: String, kind: Symbol).void }
158
+ def add_test_code_lens(node, name:, command:, kind:)
159
+ arguments = [
160
+ @path,
161
+ name,
162
+ command,
163
+ {
164
+ start_line: node.location.start_line - 1,
165
+ start_column: node.location.start_column,
166
+ end_line: node.location.end_line - 1,
167
+ end_column: node.location.end_column,
168
+ },
169
+ ]
170
+
119
171
  @response << create_code_lens(
120
172
  node,
121
173
  title: "Run",
122
174
  command_name: "rubyLsp.runTest",
123
- path: @path,
124
- name: name,
125
- test_command: command,
126
- type: "test",
175
+ arguments: arguments,
176
+ data: { type: "test", kind: kind },
127
177
  )
128
178
 
129
179
  @response << create_code_lens(
130
180
  node,
131
181
  title: "Run In Terminal",
132
182
  command_name: "rubyLsp.runTestInTerminal",
133
- path: @path,
134
- name: name,
135
- test_command: command,
136
- type: "test_in_terminal",
183
+ arguments: arguments,
184
+ data: { type: "test_in_terminal", kind: kind },
137
185
  )
138
186
 
139
187
  @response << create_code_lens(
140
188
  node,
141
189
  title: "Debug",
142
190
  command_name: "rubyLsp.debugTest",
143
- path: @path,
144
- name: name,
145
- test_command: command,
146
- type: "debug",
191
+ arguments: arguments,
192
+ data: { type: "debug", kind: kind },
193
+ )
194
+ end
195
+
196
+ sig { params(node: SyntaxTree::Command).returns(T.nilable(String)) }
197
+ def resolve_gem_remote(node)
198
+ gem_statement = node.arguments.parts.flat_map(&:child_nodes).first
199
+ return unless gem_statement
200
+
201
+ spec = Gem::Specification.stubs.find { |gem| gem.name == gem_statement.value }&.to_spec
202
+ return if spec.nil?
203
+
204
+ [spec.homepage, spec.metadata["source_code_uri"]].compact.find do |page|
205
+ page.start_with?("https://github.com", "https://gitlab.com")
206
+ end
207
+ end
208
+
209
+ sig { params(class_name: String, method_name: T.nilable(String)).returns(String) }
210
+ def generate_test_command(class_name:, method_name: nil)
211
+ command = BASE_COMMAND + @path
212
+
213
+ case @test_library
214
+ when "minitest"
215
+ command += if method_name
216
+ " --name " + "/#{Shellwords.escape(class_name + "#" + method_name)}/"
217
+ else
218
+ " --name " + "/#{Shellwords.escape(class_name)}/"
219
+ end
220
+ when "test-unit"
221
+ command += " --testcase " + "/#{Shellwords.escape(class_name)}/"
222
+
223
+ if method_name
224
+ command += " --name " + Shellwords.escape(method_name)
225
+ end
226
+ end
227
+
228
+ command
229
+ end
230
+
231
+ sig { params(node: SyntaxTree::Command, remote: String).void }
232
+ def add_open_gem_remote_code_lens(node, remote)
233
+ @response << create_code_lens(
234
+ node,
235
+ title: "Open remote",
236
+ command_name: "rubyLsp.openLink",
237
+ arguments: [remote],
238
+ data: { type: "link" },
147
239
  )
148
240
  end
149
241
  end
@@ -30,7 +30,9 @@ module RubyLsp
30
30
 
31
31
  sig { override.returns(T.nilable(T.all(T::Array[Support::RuboCopDiagnostic], Object))) }
32
32
  def run
33
+ # Running RuboCop is slow, so to avoid excessive runs we only do so if the file is syntactically valid
33
34
  return if @document.syntax_error?
35
+
34
36
  return unless defined?(Support::RuboCopDiagnosticsRunner)
35
37
 
36
38
  # Don't try to run RuboCop diagnostics for files outside the current working directory
@@ -27,7 +27,6 @@ module RubyLsp
27
27
  SyntaxTree::For,
28
28
  SyntaxTree::HashLiteral,
29
29
  SyntaxTree::Heredoc,
30
- SyntaxTree::IfNode,
31
30
  SyntaxTree::ModuleDeclaration,
32
31
  SyntaxTree::SClass,
33
32
  SyntaxTree::UnlessNode,
@@ -42,6 +41,7 @@ module RubyLsp
42
41
 
43
42
  NODES_WITH_STATEMENTS = T.let(
44
43
  [
44
+ SyntaxTree::IfNode,
45
45
  SyntaxTree::Elsif,
46
46
  SyntaxTree::In,
47
47
  SyntaxTree::Rescue,
@@ -52,6 +52,7 @@ module RubyLsp
52
52
 
53
53
  StatementNode = T.type_alias do
54
54
  T.any(
55
+ SyntaxTree::IfNode,
55
56
  SyntaxTree::Elsif,
56
57
  SyntaxTree::In,
57
58
  SyntaxTree::Rescue,