ruby-lsp 0.6.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 11dd3401d408ed456da9b4dd0aca906b991a45a0103ca8786eafbf34d4659c85
4
- data.tar.gz: 8469dd62a3660965daee589835e1481fe09e17b55cbd9d61c461d4ba94db833e
3
+ metadata.gz: 812bcf5c0bc5512ad4382440118051b99d79e79d37a2e91e1fbe4341458dd94c
4
+ data.tar.gz: 73a83f9e143bde544d72819e0c867182a0bc8b82ff2ef2dc98032aecdc28e825
5
5
  SHA512:
6
- metadata.gz: 6d2e0c3d9c5bf11068b1d5e3092d4b5f3b233066a33b7e74ea218f115fbb8b154798a8b8b0634569781245f1dedcb6da3e5ca3a23db04f78fc105c496d2a3280
7
- data.tar.gz: 529ce841833605ac40c67536bbca1eb1a9568b742de854f4698ced147813ec63569fd926d4035d518f7bb0ab54d2bbd4665e7ad2564923b49036573a0d4625e7
6
+ metadata.gz: 03b1252a63bc78186983a8962b91f2e89efa679e2ee3b4978d655a83e9a6cbdeb6a8171647e416dd4ebf43ae82fc36e04b805444acbc2a7eade386166873026e
7
+ data.tar.gz: 443e7b58bd600a27827043915897c1d848fe8f07c00d348677a9322dcff005bdb4427d2ceade5347f8256fbb6a68897f85215bdd563110d02b0a1887abb71224
data/README.md CHANGED
@@ -27,7 +27,11 @@ The gem can be installed by doing
27
27
  gem install ruby-lsp
28
28
  ```
29
29
 
30
- If you decide to add the gem to the bundle, it is not necessary to require it.
30
+ **NOTE**: starting with v0.7.0, it is no longer recommended to add the `ruby-lsp` to the bundle. The gem will generate a
31
+ custom bundle in `.ruby-lsp/Gemfile` which is used to identify the versions of dependencies that should be used for the
32
+ application (e.g.: the correct RuboCop version).
33
+
34
+ For older versions, if you decide to add the gem to the bundle, it is not necessary to require it.
31
35
  ```ruby
32
36
  group :development do
33
37
  gem "ruby-lsp", require: false
@@ -42,6 +46,15 @@ See the [documentation](https://shopify.github.io/ruby-lsp) for more in-depth de
42
46
  For creating rich themes for Ruby using the semantic highlighting information, see the [semantic highlighting
43
47
  documentation](SEMANTIC_HIGHLIGHTING.md).
44
48
 
49
+ ### Extensions
50
+
51
+ The Ruby LSP provides a server extension system that allows other gems to enhance the base functionality with more
52
+ editor features. This is the mechanism that powers extensions like
53
+
54
+ - [Ruby LSP Rails](https://github.com/Shopify/ruby-lsp-rails)
55
+
56
+ For instructions on how to create extensions, see the [server extensions documentation](SERVER_EXTENSIONS.md).
57
+
45
58
  ## Learn More
46
59
 
47
60
  * [RubyConf 2022: Improving the development experience with language servers](https://www.youtube.com/watch?v=kEfXPTm1aCI) ([Vinicius Stock](https://github.com/vinistock))
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.6.1
1
+ 0.7.0
data/exe/ruby-lsp CHANGED
@@ -1,6 +1,19 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
+ # When we're running without bundler, then we need to make sure the custom bundle is fully configured and re-execute
5
+ # using `BUNDLE_GEMFILE=.ruby-lsp/Gemfile bundle exec ruby-lsp` so that we have access to the gems that are a part of
6
+ # the application's bundle
7
+ if ENV["BUNDLE_GEMFILE"].nil? && File.exist?("Gemfile.lock")
8
+ require_relative "../lib/ruby_lsp/setup_bundler"
9
+
10
+ # In some cases, like when the `ruby-lsp` is already a part of the bundle, we don't generate `.ruby-lsp/Gemfile`.
11
+ # However, we still want to run the server with `bundle exec`. We need to make sure we're pointing to the right
12
+ # `Gemfile`
13
+ bundle_gemfile = File.exist?(".ruby-lsp/Gemfile") ? ".ruby-lsp/Gemfile" : "Gemfile"
14
+ exit exec("BUNDLE_GEMFILE=#{bundle_gemfile} bundle exec ruby-lsp #{ARGV.join(" ")}")
15
+ end
16
+
4
17
  require "sorbet-runtime"
5
18
 
6
19
  begin
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # This executable checks if all automatic LSP requests run successfully on every Ruby file under the current directory
5
+
6
+ require "sorbet-runtime"
7
+
8
+ begin
9
+ T::Configuration.default_checked_level = :never
10
+ T::Configuration.call_validation_error_handler = ->(*) {}
11
+ T::Configuration.inline_type_error_handler = ->(*) {}
12
+ T::Configuration.sig_validation_error_handler = ->(*) {}
13
+ rescue
14
+ nil
15
+ end
16
+
17
+ require_relative "../lib/ruby_lsp/internal"
18
+
19
+ RubyLsp::Extension.load_extensions
20
+
21
+ T::Utils.run_all_sig_blocks
22
+
23
+ files = Dir.glob("#{Dir.pwd}/**/*.rb")
24
+
25
+ puts "Verifying that all automatic LSP requests execute successfully. This may take a while..."
26
+
27
+ errors = {}
28
+ store = RubyLsp::Store.new
29
+ message_queue = Thread::Queue.new
30
+ executor = RubyLsp::Executor.new(store, message_queue)
31
+
32
+ files.each_with_index do |file, index|
33
+ uri = "file://#{file}"
34
+ store.set(uri: uri, source: File.read(file), version: 1)
35
+
36
+ # Executing any of the automatic requests will execute all of them, so here we just pick one
37
+ result = executor.execute({
38
+ method: "textDocument/documentSymbol",
39
+ params: { textDocument: { uri: uri } },
40
+ })
41
+
42
+ error = result.error
43
+ errors[file] = error if error
44
+ ensure
45
+ store.delete(uri)
46
+ print("\033[M\033[0KCompleted #{index + 1}/#{files.length}") unless ENV["CI"]
47
+ end
48
+
49
+ puts "\n"
50
+ message_queue.close
51
+
52
+ if errors.empty?
53
+ puts "All automatic LSP requests executed successfully"
54
+ exit
55
+ end
56
+
57
+ puts <<~ERRORS
58
+ Errors while executing requests:
59
+
60
+ #{errors.map { |file, error| "#{file}: #{error.message}" }.join("\n")}
61
+ ERRORS
62
+ exit!
@@ -52,7 +52,7 @@ module RubyLsp
52
52
  # Find all classes that inherit from BaseRequest or Listener, which are the ones we want to make sure are
53
53
  # documented
54
54
  features = ObjectSpace.each_object(Class).filter_map do |k|
55
- klass = T.cast(k, T::Class[T.anything])
55
+ klass = T.unsafe(k)
56
56
  klass if klass < RubyLsp::Requests::BaseRequest || klass < RubyLsp::Listener
57
57
  end
58
58
 
@@ -114,12 +114,12 @@ module RubyLsp
114
114
  params(
115
115
  position: PositionShape,
116
116
  node_types: T::Array[T.class_of(SyntaxTree::Node)],
117
- ).returns([T.nilable(SyntaxTree::Node), T.nilable(SyntaxTree::Node)])
117
+ ).returns([T.nilable(SyntaxTree::Node), T.nilable(SyntaxTree::Node), T::Array[String]])
118
118
  end
119
119
  def locate_node(position, node_types: [])
120
- return [nil, nil] unless parsed?
120
+ return [nil, nil, []] unless parsed?
121
121
 
122
- locate(T.must(@tree), create_scanner.find_char_position(position))
122
+ locate(T.must(@tree), create_scanner.find_char_position(position), node_types: node_types)
123
123
  end
124
124
 
125
125
  sig do
@@ -127,12 +127,13 @@ module RubyLsp
127
127
  node: SyntaxTree::Node,
128
128
  char_position: Integer,
129
129
  node_types: T::Array[T.class_of(SyntaxTree::Node)],
130
- ).returns([T.nilable(SyntaxTree::Node), T.nilable(SyntaxTree::Node)])
130
+ ).returns([T.nilable(SyntaxTree::Node), T.nilable(SyntaxTree::Node), T::Array[String]])
131
131
  end
132
132
  def locate(node, char_position, node_types: [])
133
133
  queue = T.let(node.child_nodes.compact, T::Array[T.nilable(SyntaxTree::Node)])
134
134
  closest = node
135
135
  parent = T.let(nil, T.nilable(SyntaxTree::Node))
136
+ nesting = T.let([], T::Array[T.any(SyntaxTree::ClassDeclaration, SyntaxTree::ModuleDeclaration)])
136
137
 
137
138
  until queue.empty?
138
139
  candidate = queue.shift
@@ -140,8 +141,10 @@ module RubyLsp
140
141
  # Skip nil child nodes
141
142
  next if candidate.nil?
142
143
 
143
- # Add the next child_nodes to the queue to be processed
144
- queue.concat(candidate.child_nodes)
144
+ # Add the next child_nodes to the queue to be processed. The order here is important! We want to move in the
145
+ # same order as the visiting mechanism, which means searching the child nodes before moving on to the next
146
+ # sibling
147
+ queue.unshift(*candidate.child_nodes)
145
148
 
146
149
  # Skip if the current node doesn't cover the desired position
147
150
  loc = candidate.location
@@ -151,6 +154,17 @@ module RubyLsp
151
154
  # already
152
155
  break if char_position < loc.start_char
153
156
 
157
+ # If the candidate starts after the end of the previous nesting level, then we've exited that nesting level and
158
+ # need to pop the stack
159
+ previous_level = nesting.last
160
+ nesting.pop if previous_level && candidate.start_char > previous_level.end_char
161
+
162
+ # Keep track of the nesting where we found the target. This is used to determine the fully qualified name of the
163
+ # target when it is a constant
164
+ if candidate.is_a?(SyntaxTree::ClassDeclaration) || candidate.is_a?(SyntaxTree::ModuleDeclaration)
165
+ nesting << candidate
166
+ end
167
+
154
168
  # If there are node types to filter by, and the current node is not one of those types, then skip it
155
169
  next if node_types.any? && node_types.none? { |type| candidate.class == type }
156
170
 
@@ -162,7 +176,7 @@ module RubyLsp
162
176
  end
163
177
  end
164
178
 
165
- [closest, parent]
179
+ [closest, parent, nesting.map { |n| n.constant.constant.value }]
166
180
  end
167
181
 
168
182
  class Scanner
@@ -53,6 +53,12 @@ module RubyLsp
53
53
 
54
54
  # Visit dispatchers are below. Notice that for nodes that create a new scope (e.g.: classes, modules, method defs)
55
55
  # we need both an `on_*` and `after_*` event. This is because some requests must know when we exit the scope
56
+ sig { override.params(node: T.nilable(SyntaxTree::Node)).void }
57
+ def visit(node)
58
+ @listeners[:on_node]&.each { |l| T.unsafe(l).on_node(node) }
59
+ super
60
+ end
61
+
56
62
  sig { override.params(node: SyntaxTree::ClassDeclaration).void }
57
63
  def visit_class(node)
58
64
  @listeners[:on_class]&.each { |l| T.unsafe(l).on_class(node) }
@@ -97,13 +97,11 @@ module RubyLsp
97
97
  document_symbol = Requests::DocumentSymbol.new(emitter, @message_queue)
98
98
  document_link = Requests::DocumentLink.new(uri, emitter, @message_queue)
99
99
  code_lens = Requests::CodeLens.new(uri, emitter, @message_queue, @test_library)
100
- code_lens_extensions_listeners = Requests::CodeLens.listeners.map do |l|
101
- T.unsafe(l).new(document.uri, emitter, @message_queue)
102
- end
100
+
103
101
  semantic_highlighting = Requests::SemanticHighlighting.new(emitter, @message_queue)
104
102
  emitter.visit(document.tree) if document.parsed?
105
103
 
106
- code_lens_extensions_listeners.each { |ext| code_lens.merge_response!(ext) }
104
+ code_lens.merge_external_listeners_responses!
107
105
 
108
106
  # Store all responses retrieve in this round of visits in the cache and then return the response for the request
109
107
  # we actually received
@@ -130,7 +128,7 @@ module RubyLsp
130
128
  )
131
129
 
132
130
  nil
133
- rescue StandardError => error
131
+ rescue StandardError, LoadError => error
134
132
  @message_queue << Notification.new(
135
133
  message: "window/showMessage",
136
134
  params: Interface::ShowMessageParams.new(
@@ -156,7 +154,7 @@ module RubyLsp
156
154
  when "textDocument/diagnostic"
157
155
  begin
158
156
  diagnostic(uri)
159
- rescue StandardError => error
157
+ rescue StandardError, LoadError => error
160
158
  @message_queue << Notification.new(
161
159
  message: "window/showMessage",
162
160
  params: Interface::ShowMessageParams.new(
@@ -169,9 +167,26 @@ module RubyLsp
169
167
  end
170
168
  when "textDocument/completion"
171
169
  completion(uri, request.dig(:params, :position))
170
+ when "textDocument/definition"
171
+ definition(uri, request.dig(:params, :position))
172
+ when "rubyLsp/textDocument/showSyntaxTree"
173
+ { ast: Requests::ShowSyntaxTree.new(@store.get(uri)).run }
172
174
  end
173
175
  end
174
176
 
177
+ sig { params(uri: String, position: Document::PositionShape).returns(T.nilable(Interface::Location)) }
178
+ def definition(uri, position)
179
+ document = @store.get(uri)
180
+ return if document.syntax_error?
181
+
182
+ target, _parent = document.locate_node(position, node_types: [SyntaxTree::Command])
183
+
184
+ emitter = EventEmitter.new
185
+ base_listener = Requests::Definition.new(uri, emitter, @message_queue)
186
+ emitter.emit_for_target(target)
187
+ base_listener.response
188
+ end
189
+
175
190
  sig { params(uri: String).returns(T::Array[Interface::FoldingRange]) }
176
191
  def folding_range(uri)
177
192
  @store.cache_fetch(uri, "textDocument/foldingRange") do |document|
@@ -198,15 +213,13 @@ module RubyLsp
198
213
 
199
214
  # Instantiate all listeners
200
215
  emitter = EventEmitter.new
201
- base_listener = Requests::Hover.new(emitter, @message_queue)
202
- listeners = Requests::Hover.listeners.map { |l| l.new(emitter, @message_queue) }
216
+ hover = Requests::Hover.new(emitter, @message_queue)
203
217
 
204
218
  # Emit events for all listeners
205
219
  emitter.emit_for_target(target)
206
220
 
207
- # Merge all responses into a single hover
208
- listeners.each { |ext| base_listener.merge_response!(ext) }
209
- base_listener.response
221
+ hover.merge_external_listeners_responses!
222
+ hover.response
210
223
  end
211
224
 
212
225
  sig { params(uri: String, content_changes: T::Array[Document::EditShape], version: Integer).returns(Object) }
@@ -275,10 +288,17 @@ module RubyLsp
275
288
  params(
276
289
  uri: String,
277
290
  position: Document::PositionShape,
278
- ).returns(T::Array[Interface::DocumentHighlight])
291
+ ).returns(T.nilable(T::Array[Interface::DocumentHighlight]))
279
292
  end
280
293
  def document_highlight(uri, position)
281
- Requests::DocumentHighlight.new(@store.get(uri), position).run
294
+ document = @store.get(uri)
295
+ return if document.syntax_error?
296
+
297
+ target, parent = document.locate_node(position)
298
+ emitter = EventEmitter.new
299
+ listener = Requests::DocumentHighlight.new(target, parent, emitter, @message_queue)
300
+ emitter.visit(document.tree)
301
+ listener.response
282
302
  end
283
303
 
284
304
  sig { params(uri: String, range: Document::RangeShape).returns(T.nilable(T::Array[Interface::InlayHint])) }
@@ -429,7 +449,9 @@ module RubyLsp
429
449
  end
430
450
 
431
451
  configured_features = options.dig(:initializationOptions, :enabledFeatures)
432
- experimental_features = options.dig(:initializationOptions, :experimentalFeaturesEnabled)
452
+
453
+ # Uncomment the line below and use the variable to gate features behind the experimental flag
454
+ # experimental_features = options.dig(:initializationOptions, :experimentalFeaturesEnabled)
433
455
 
434
456
  enabled_features = case configured_features
435
457
  when Array
@@ -458,7 +480,7 @@ module RubyLsp
458
480
  Interface::DocumentLinkOptions.new(resolve_provider: false)
459
481
  end
460
482
 
461
- code_lens_provider = if experimental_features
483
+ code_lens_provider = if enabled_features["codeLens"]
462
484
  Interface::CodeLensOptions.new(resolve_provider: false)
463
485
  end
464
486
 
@@ -532,6 +554,7 @@ module RubyLsp
532
554
  inlay_hint_provider: inlay_hint_provider,
533
555
  completion_provider: completion_provider,
534
556
  code_lens_provider: code_lens_provider,
557
+ definition_provider: enabled_features["definition"],
535
558
  ),
536
559
  )
537
560
  end
@@ -29,6 +29,7 @@ module RubyLsp
29
29
 
30
30
  BASE_COMMAND = T.let((File.exist?("Gemfile.lock") ? "bundle exec ruby" : "ruby") + " -Itest ", String)
31
31
  ACCESS_MODIFIERS = T.let(["public", "private", "protected"], T::Array[String])
32
+ SUPPORTED_TEST_LIBRARIES = T.let(["minitest", "test-unit"], T::Array[String])
32
33
 
33
34
  sig { override.returns(ResponseType) }
34
35
  attr_reader :response
@@ -37,6 +38,8 @@ module RubyLsp
37
38
  def initialize(uri, emitter, message_queue, test_library)
38
39
  super(emitter, message_queue)
39
40
 
41
+ @uri = T.let(uri, String)
42
+ @external_listeners = T.let([], T::Array[RubyLsp::Listener[ResponseType]])
40
43
  @test_library = T.let(test_library, String)
41
44
  @response = T.let([], ResponseType)
42
45
  @path = T.let(T.must(URI(uri).path), String)
@@ -55,14 +58,31 @@ module RubyLsp
55
58
  :after_call,
56
59
  :on_vcall,
57
60
  )
61
+
62
+ register_external_listeners!
63
+ end
64
+
65
+ sig { void }
66
+ def register_external_listeners!
67
+ self.class.listeners.each do |l|
68
+ @external_listeners << T.unsafe(l).new(@uri, @emitter, @message_queue)
69
+ end
70
+ end
71
+
72
+ sig { void }
73
+ def merge_external_listeners_responses!
74
+ @external_listeners.each do |l|
75
+ merge_response!(l)
76
+ end
58
77
  end
59
78
 
60
79
  sig { params(node: SyntaxTree::ClassDeclaration).void }
61
80
  def on_class(node)
62
81
  @visibility_stack.push(["public", "public"])
63
82
  class_name = node.constant.constant.value
83
+ @class_stack.push(class_name)
84
+
64
85
  if class_name.end_with?("Test")
65
- @class_stack.push(class_name)
66
86
  add_test_code_lens(
67
87
  node,
68
88
  name: class_name,
@@ -81,7 +101,7 @@ module RubyLsp
81
101
  sig { params(node: SyntaxTree::DefNode).void }
82
102
  def on_def(node)
83
103
  class_name = @class_stack.last
84
- return unless class_name
104
+ return unless class_name&.end_with?("Test")
85
105
 
86
106
  visibility, _ = @visibility_stack.last
87
107
  if visibility == "public"
@@ -156,6 +176,9 @@ module RubyLsp
156
176
 
157
177
  sig { params(node: SyntaxTree::Node, name: String, command: String, kind: Symbol).void }
158
178
  def add_test_code_lens(node, name:, command:, kind:)
179
+ # don't add code lenses if the test library is not supported or unknown
180
+ return unless SUPPORTED_TEST_LIBRARIES.include?(@test_library)
181
+
159
182
  arguments = [
160
183
  @path,
161
184
  name,
@@ -195,10 +218,13 @@ module RubyLsp
195
218
 
196
219
  sig { params(node: SyntaxTree::Command).returns(T.nilable(String)) }
197
220
  def resolve_gem_remote(node)
198
- gem_statement = node.arguments.parts.flat_map(&:child_nodes).first
199
- return unless gem_statement
221
+ gem_statement = node.arguments.parts.first
222
+ return unless gem_statement.is_a?(SyntaxTree::StringLiteral)
223
+
224
+ gem_name = gem_statement.parts.first
225
+ return unless gem_name.is_a?(SyntaxTree::TStringContent)
200
226
 
201
- spec = Gem::Specification.stubs.find { |gem| gem.name == gem_statement.value }&.to_spec
227
+ spec = Gem::Specification.stubs.find { |gem| gem.name == gem_name.value }&.to_spec
202
228
  return if spec.nil?
203
229
 
204
230
  [spec.homepage, spec.metadata["source_code_uri"]].compact.find do |page|
@@ -0,0 +1,95 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ module Requests
6
+ # ![Definition demo](../../definition.gif)
7
+ #
8
+ # The [definition
9
+ # request](https://microsoft.github.io/language-server-protocol/specification#textDocument_definition) jumps to the
10
+ # definition of the symbol under the cursor.
11
+ #
12
+ # Currently, only jumping to required files is supported.
13
+ #
14
+ # # Example
15
+ #
16
+ # ```ruby
17
+ # require "some_gem/file" # <- Request go to definition on this string will take you to the file
18
+ # ```
19
+ class Definition < Listener
20
+ extend T::Sig
21
+ extend T::Generic
22
+
23
+ ResponseType = type_member { { fixed: T.nilable(Interface::Location) } }
24
+
25
+ sig { override.returns(ResponseType) }
26
+ attr_reader :response
27
+
28
+ sig { params(uri: String, emitter: EventEmitter, message_queue: Thread::Queue).void }
29
+ def initialize(uri, emitter, message_queue)
30
+ super(emitter, message_queue)
31
+
32
+ @uri = uri
33
+ @response = T.let(nil, ResponseType)
34
+ emitter.register(self, :on_command)
35
+ end
36
+
37
+ sig { params(node: SyntaxTree::Command).void }
38
+ def on_command(node)
39
+ message = node.message.value
40
+ return unless message == "require" || message == "require_relative"
41
+
42
+ argument = node.arguments.parts.first
43
+ return unless argument.is_a?(SyntaxTree::StringLiteral)
44
+
45
+ string = argument.parts.first
46
+ return unless string.is_a?(SyntaxTree::TStringContent)
47
+
48
+ required_file = "#{string.value}.rb"
49
+
50
+ case message
51
+ when "require"
52
+ candidate = find_file_in_load_path(required_file)
53
+
54
+ if candidate
55
+ @response = Interface::Location.new(
56
+ uri: "file://#{candidate}",
57
+ range: Interface::Range.new(
58
+ start: Interface::Position.new(line: 0, character: 0),
59
+ end: Interface::Position.new(line: 0, character: 0),
60
+ ),
61
+ )
62
+ end
63
+ when "require_relative"
64
+ current_file = T.must(URI.parse(@uri).path)
65
+ current_folder = Pathname.new(current_file).dirname
66
+ candidate = File.expand_path(File.join(current_folder, required_file))
67
+
68
+ if candidate
69
+ @response = Interface::Location.new(
70
+ uri: "file://#{candidate}",
71
+ range: Interface::Range.new(
72
+ start: Interface::Position.new(line: 0, character: 0),
73
+ end: Interface::Position.new(line: 0, character: 0),
74
+ ),
75
+ )
76
+ end
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ sig { params(file: String).returns(T.nilable(String)) }
83
+ def find_file_in_load_path(file)
84
+ return unless file.include?("/")
85
+
86
+ $LOAD_PATH.each do |p|
87
+ found = Dir.glob("**/#{file}", base: p).first
88
+ return "#{p}/#{found}" if found
89
+ end
90
+
91
+ nil
92
+ end
93
+ end
94
+ end
95
+ end
@@ -22,34 +22,49 @@ module RubyLsp
22
22
  # FOO # should be highlighted as "read"
23
23
  # end
24
24
  # ```
25
- class DocumentHighlight < BaseRequest
25
+ class DocumentHighlight < Listener
26
26
  extend T::Sig
27
27
 
28
- sig { params(document: Document, position: Document::PositionShape).void }
29
- def initialize(document, position)
30
- super(document)
28
+ ResponseType = type_member { { fixed: T::Array[Interface::DocumentHighlight] } }
31
29
 
32
- @highlights = T.let([], T::Array[Interface::DocumentHighlight])
33
- return unless document.parsed?
30
+ sig { override.returns(ResponseType) }
31
+ attr_reader :response
34
32
 
35
- @target = T.let(find(position), T.nilable(Support::HighlightTarget))
33
+ sig do
34
+ params(
35
+ target: T.nilable(SyntaxTree::Node),
36
+ parent: T.nilable(SyntaxTree::Node),
37
+ emitter: EventEmitter,
38
+ message_queue: Thread::Queue,
39
+ ).void
36
40
  end
41
+ def initialize(target, parent, emitter, message_queue)
42
+ super(emitter, message_queue)
43
+
44
+ @response = T.let([], T::Array[Interface::DocumentHighlight])
45
+
46
+ return unless target && parent
37
47
 
38
- sig { override.returns(T.all(T::Array[Interface::DocumentHighlight], Object)) }
39
- def run
40
- # no @target means the target is not highlightable
41
- visit(@document.tree) if @document.parsed? && @target
42
- @highlights
48
+ highlight_target =
49
+ case target
50
+ when *DIRECT_HIGHLIGHTS
51
+ Support::HighlightTarget.new(target)
52
+ when SyntaxTree::Ident
53
+ relevant_node = parent.is_a?(SyntaxTree::Params) ? target : parent
54
+ Support::HighlightTarget.new(relevant_node)
55
+ end
56
+
57
+ @target = T.let(highlight_target, T.nilable(Support::HighlightTarget))
58
+
59
+ emitter.register(self, :on_node) if @target
43
60
  end
44
61
 
45
- sig { override.params(node: T.nilable(SyntaxTree::Node)).void }
46
- def visit(node)
62
+ sig { params(node: T.nilable(SyntaxTree::Node)).void }
63
+ def on_node(node)
47
64
  return if node.nil?
48
65
 
49
66
  match = T.must(@target).highlight_type(node)
50
67
  add_highlight(match) if match
51
-
52
- super
53
68
  end
54
69
 
55
70
  private
@@ -65,30 +80,10 @@ module RubyLsp
65
80
  T::Array[T.class_of(SyntaxTree::Node)],
66
81
  )
67
82
 
68
- sig do
69
- params(
70
- position: Document::PositionShape,
71
- ).returns(T.nilable(Support::HighlightTarget))
72
- end
73
- def find(position)
74
- matched, parent = @document.locate_node(position)
75
-
76
- return unless matched && parent
77
- return unless matched.is_a?(SyntaxTree::Ident) || DIRECT_HIGHLIGHTS.include?(matched.class)
78
-
79
- case matched
80
- when *DIRECT_HIGHLIGHTS
81
- Support::HighlightTarget.new(matched)
82
- when SyntaxTree::Ident
83
- relevant_node = parent.is_a?(SyntaxTree::Params) ? matched : parent
84
- Support::HighlightTarget.new(relevant_node)
85
- end
86
- end
87
-
88
83
  sig { params(match: Support::HighlightTarget::HighlightMatch).void }
89
84
  def add_highlight(match)
90
85
  range = range_from_syntax_tree_node(match.node)
91
- @highlights << Interface::DocumentHighlight.new(range: range, kind: match.type)
86
+ @response << Interface::DocumentHighlight.new(range: range, kind: match.type)
92
87
  end
93
88
  end
94
89
  end
@@ -39,8 +39,25 @@ module RubyLsp
39
39
  def initialize(emitter, message_queue)
40
40
  super
41
41
 
42
+ @external_listeners = T.let([], T::Array[RubyLsp::Listener[ResponseType]])
42
43
  @response = T.let(nil, ResponseType)
43
44
  emitter.register(self, :on_command, :on_const_path_ref, :on_call)
45
+
46
+ register_external_listeners!
47
+ end
48
+
49
+ sig { void }
50
+ def register_external_listeners!
51
+ self.class.listeners.each do |l|
52
+ @external_listeners << T.unsafe(l).new(@emitter, @message_queue)
53
+ end
54
+ end
55
+
56
+ sig { void }
57
+ def merge_external_listeners_responses!
58
+ @external_listeners.each do |l|
59
+ merge_response!(l)
60
+ end
44
61
  end
45
62
 
46
63
  # Merges responses from other hover listeners
@@ -30,13 +30,11 @@ module RubyLsp
30
30
  def initialize(document, position, trigger_character)
31
31
  super(document)
32
32
 
33
- scanner = document.create_scanner
34
- line_begin = position[:line] == 0 ? 0 : scanner.find_char_position({ line: position[:line] - 1, character: 0 })
35
- @line_end = T.let(scanner.find_char_position(position), Integer)
36
- line = T.must(@document.source[line_begin..@line_end])
33
+ @lines = T.let(@document.source.lines, T::Array[String])
34
+ line = @lines[[position[:line] - 1, 0].max]
37
35
 
38
- @indentation = T.let(find_indentation(line), Integer)
39
- @previous_line = T.let(line.strip.chomp, String)
36
+ @indentation = T.let(line ? find_indentation(line) : 0, Integer)
37
+ @previous_line = T.let(line ? line.strip.chomp : "", String)
40
38
  @position = position
41
39
  @edits = T.let([], T::Array[Interface::TextEdit])
42
40
  @trigger_character = trigger_character
@@ -64,9 +62,34 @@ module RubyLsp
64
62
 
65
63
  sig { void }
66
64
  def handle_pipe
67
- return unless /((?<=do)|(?<={))\s+\|/.match?(@previous_line)
65
+ current_line = @lines[@position[:line]]
66
+ return unless /((?<=do)|(?<={))\s+\|/.match?(current_line)
67
+
68
+ line = T.must(current_line)
69
+
70
+ # If the current character is a pipe and both previous ones are pipes too, then we autocompleted a pipe and the
71
+ # user inserted a third one. In this case, we need to avoid adding a fourth and remove the previous one
72
+ if line[@position[:character] - 2] == "|" &&
73
+ line[@position[:character] - 1] == "|" &&
74
+ line[@position[:character]] == "|"
75
+
76
+ @edits << Interface::TextEdit.new(
77
+ range: Interface::Range.new(
78
+ start: Interface::Position.new(
79
+ line: @position[:line],
80
+ character: @position[:character],
81
+ ),
82
+ end: Interface::Position.new(
83
+ line: @position[:line],
84
+ character: @position[:character] + 1,
85
+ ),
86
+ ),
87
+ new_text: "",
88
+ )
89
+ else
90
+ add_edit_with_text("|")
91
+ end
68
92
 
69
- add_edit_with_text("|")
70
93
  move_cursor_to(@position[:line], @position[:character])
71
94
  end
72
95
 
@@ -87,30 +110,15 @@ module RubyLsp
87
110
  return unless END_REGEXES.any? { |regex| regex.match?(@previous_line) }
88
111
 
89
112
  indents = " " * @indentation
113
+ current_line = @lines[@position[:line]]
114
+ next_line = @lines[@position[:line] + 1]
90
115
 
91
- if @previous_line.include?("\n")
92
- # If the previous line has a line break, then it means there's content after the line break that triggered
93
- # this completion. For these cases, we want to add the `end` after the content and move the cursor back to the
94
- # keyword that triggered the completion
95
-
96
- line = @position[:line]
97
-
98
- # If there are enough lines in the document, we want to add the `end` token on the line below the extra
99
- # content. Otherwise, we want to insert and extra line break ourselves
100
- correction = if T.must(@document.source[@line_end..-1]).count("\n") >= 2
101
- line -= 1
102
- "#{indents}end"
103
- else
104
- "#{indents}\nend"
105
- end
106
-
107
- add_edit_with_text(correction, { line: @position[:line] + 1, character: @position[:character] })
108
- move_cursor_to(line, @indentation + 3)
109
- else
110
- # If there's nothing after the new line break that triggered the completion, then we want to add the `end` and
111
- # move the cursor to the body of the statement
116
+ if current_line.nil? || current_line.strip.empty?
112
117
  add_edit_with_text(" \n#{indents}end")
113
118
  move_cursor_to(@position[:line], @indentation + 2)
119
+ elsif next_line.nil? || next_line.strip.empty?
120
+ add_edit_with_text("#{indents}end", { line: @position[:line] + 1, character: @position[:character] })
121
+ move_cursor_to(@position[:line], @indentation + 3)
114
122
  end
115
123
  end
116
124
 
@@ -0,0 +1,33 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ module Requests
6
+ # ![Show syntax tree demo](../../show_syntax_tree.gif)
7
+ #
8
+ # Show syntax tree is a custom [LSP
9
+ # request](https://microsoft.github.io/language-server-protocol/specification#requestMessage) that displays the AST
10
+ # for the current document in a new tab.
11
+ #
12
+ # # Example
13
+ #
14
+ # ```ruby
15
+ # # Executing the Ruby LSP: Show syntax tree command will display the AST for the document
16
+ # 1 + 1
17
+ # # (program (statements ((binary (int "1") + (int "1")))))
18
+ # ```
19
+ #
20
+ class ShowSyntaxTree < BaseRequest
21
+ extend T::Sig
22
+
23
+ sig { override.returns(String) }
24
+ def run
25
+ return "Document contains syntax error" if @document.syntax_error?
26
+
27
+ output_string = +""
28
+ PP.pp(@document.tree, output_string)
29
+ output_string
30
+ end
31
+ end
32
+ end
33
+ end
@@ -20,15 +20,19 @@ module RubyLsp
20
20
 
21
21
  sig { returns(String) }
22
22
  def detected_test_library
23
- if direct_dependency?(/^minitest/)
23
+ # A Rails app may have a dependency on minitest, but we would instead want to use the Rails test runner provided
24
+ # by ruby-lsp-rails.
25
+ if direct_dependency?(/^rails$/)
26
+ "rails"
27
+ # NOTE: Intentionally ends with $ to avoid mis-matching minitest-reporters, etc. in a Rails app.
28
+ elsif direct_dependency?(/^minitest$/)
24
29
  "minitest"
25
30
  elsif direct_dependency?(/^test-unit/)
26
31
  "test-unit"
27
32
  elsif direct_dependency?(/^rspec/)
28
33
  "rspec"
29
34
  else
30
- warn("WARNING: No test library detected. Assuming minitest.")
31
- "minitest"
35
+ "unknown"
32
36
  end
33
37
  end
34
38
 
@@ -78,7 +78,7 @@ module URI
78
78
  if URI.respond_to?(:register_scheme)
79
79
  URI.register_scheme("SOURCE", self)
80
80
  else
81
- @@schemes = T.let(@@schemes, T::Hash[String, T::Class[T.anything]]) # rubocop:disable Style/ClassVars
81
+ @@schemes = T.let(@@schemes, T::Hash[String, T.untyped]) # rubocop:disable Style/ClassVars
82
82
  @@schemes["SOURCE"] = self
83
83
  end
84
84
  end
@@ -19,6 +19,8 @@ module RubyLsp
19
19
  # - [InlayHint](rdoc-ref:RubyLsp::Requests::InlayHints)
20
20
  # - [PathCompletion](rdoc-ref:RubyLsp::Requests::PathCompletion)
21
21
  # - [CodeLens](rdoc-ref:RubyLsp::Requests::CodeLens)
22
+ # - [Definition](rdoc-ref:RubyLsp::Requests::Definition)
23
+ # - [ShowSyntaxTree](rdoc-ref:RubyLsp::Requests::ShowSyntaxTree)
22
24
 
23
25
  module Requests
24
26
  autoload :BaseRequest, "ruby_lsp/requests/base_request"
@@ -37,6 +39,8 @@ module RubyLsp
37
39
  autoload :InlayHints, "ruby_lsp/requests/inlay_hints"
38
40
  autoload :PathCompletion, "ruby_lsp/requests/path_completion"
39
41
  autoload :CodeLens, "ruby_lsp/requests/code_lens"
42
+ autoload :Definition, "ruby_lsp/requests/definition"
43
+ autoload :ShowSyntaxTree, "ruby_lsp/requests/show_syntax_tree"
40
44
 
41
45
  # :nodoc:
42
46
  module Support
@@ -70,6 +70,8 @@ module RubyLsp
70
70
  when "$/cancelRequest"
71
71
  # Cancel the job if it's still in the queue
72
72
  @mutex.synchronize { @jobs[request[:params][:id]]&.cancel }
73
+ when "$/setTrace"
74
+ VOID
73
75
  when "shutdown"
74
76
  warn("Shutting down Ruby LSP...")
75
77
 
@@ -0,0 +1,86 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler"
5
+ require "fileutils"
6
+ require "pathname"
7
+
8
+ # This file is a script that will configure a custom bundle for the Ruby LSP. The custom bundle allows developers to use
9
+ # the Ruby LSP without including the gem in their application's Gemfile while at the same time giving us access to the
10
+ # exact locked versions of dependencies.
11
+
12
+ # Do not setup a custom bundle if we're working on the Ruby LSP, since it's already included by default
13
+ if Pathname.new(Dir.pwd).basename == "ruby-lsp"
14
+ warn("Ruby LSP> Skipping custom bundle setup since we're working on the Ruby LSP itself")
15
+ return
16
+ end
17
+
18
+ # We need to parse the Gemfile.lock manually here. If we try to do `bundler/setup` to use something more convenient, we
19
+ # may end up with issues when the globally installed `ruby-lsp` version mismatches the one included in the `Gemfile`
20
+ dependencies = Bundler::LockfileParser.new(Bundler.read_file("Gemfile.lock")).dependencies
21
+
22
+ # When working on a gem, the `ruby-lsp` might be listed as a dependency in the gemspec. We need to make sure we check
23
+ # those as well or else we may get version mismatch errors
24
+ gemspec_path = Dir.glob("*.gemspec").first
25
+ if gemspec_path
26
+ gemspec_dependencies = Bundler.load_gemspec(gemspec_path).dependencies.to_h { |dep| [dep.name, dep] }
27
+ dependencies.merge!(gemspec_dependencies)
28
+ end
29
+
30
+ # Do not setup a custom bundle if both `ruby-lsp` and `debug` are already in the Gemfile
31
+ if dependencies["ruby-lsp"] && dependencies["debug"]
32
+ warn("Ruby LSP> Skipping custom bundle setup since both `ruby-lsp` and `debug` are already in the Gemfile")
33
+ return
34
+ end
35
+
36
+ # Automatically create and ignore the .ruby-lsp folder for users
37
+ FileUtils.mkdir(".ruby-lsp") unless Dir.exist?(".ruby-lsp")
38
+ File.write(".ruby-lsp/.gitignore", "*") unless File.exist?(".ruby-lsp/.gitignore")
39
+
40
+ parts = [
41
+ "# This custom gemfile is automatically generated by the Ruby LSP.",
42
+ "# It should be automatically git ignored, but in any case: do not commit it to your repository.",
43
+ "",
44
+ "eval_gemfile(File.expand_path(\"../Gemfile\", __dir__))",
45
+ ]
46
+
47
+ unless dependencies["ruby-lsp"]
48
+ parts << 'gem "ruby-lsp", require: false, group: :development, source: "https://rubygems.org"'
49
+ end
50
+
51
+ unless dependencies["debug"]
52
+ parts << 'gem "debug", require: false, group: :development, platforms: :mri, source: "https://rubygems.org"'
53
+ end
54
+
55
+ gemfile_content = parts.join("\n")
56
+
57
+ unless File.exist?(".ruby-lsp/Gemfile") && File.read(".ruby-lsp/Gemfile") == gemfile_content
58
+ File.write(".ruby-lsp/Gemfile", gemfile_content)
59
+ end
60
+
61
+ # If .ruby-lsp/Gemfile.lock already exists and the top level Gemfile.lock hasn't been modified since it was last
62
+ # updated, then we're ready to boot the server
63
+ if File.exist?(".ruby-lsp/Gemfile.lock") && File.stat(".ruby-lsp/Gemfile.lock").mtime > File.stat("Gemfile.lock").mtime
64
+ warn("Ruby LSP> Skipping custom bundle setup since .ruby-lsp/Gemfile.lock already exists and is up to date")
65
+ return
66
+ end
67
+
68
+ FileUtils.cp("Gemfile.lock", ".ruby-lsp/Gemfile.lock")
69
+
70
+ # If the user has a custom bundle path configured, we need to ensure that we will use the absolute and not relative
71
+ # version of it when running bundle install. This is necessary to avoid installing the gems under the `.ruby-lsp`
72
+ # folder, which is not the user's intention. For example, if path is configured as `vendor`, we want to install it in
73
+ # the top level `vendor` and not `.ruby-lsp/vendor`
74
+ path = Bundler.settings["path"]
75
+
76
+ command = +""
77
+ # Use the absolute `BUNDLE_PATH` to prevent accidentally creating unwanted folders under `.ruby-lsp`
78
+ command << "BUNDLE_PATH=#{File.expand_path(path, Dir.pwd)} " if path
79
+ # Install gems using the custom bundle
80
+ command << "BUNDLE_GEMFILE=.ruby-lsp/Gemfile bundle install "
81
+ # Redirect stdout to stderr to prevent going into an infinite loop. The extension might confuse stdout output with
82
+ # responses
83
+ command << "1>&2"
84
+
85
+ warn("Ruby LSP> Running bundle install for the custom bundle. This may take a while...")
86
+ system(command)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-lsp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-06-19 00:00:00.000000000 Z
11
+ date: 2023-07-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: language_server-protocol
@@ -63,6 +63,7 @@ email:
63
63
  - ruby@shopify.com
64
64
  executables:
65
65
  - ruby-lsp
66
+ - ruby-lsp-check
66
67
  extensions: []
67
68
  extra_rdoc_files: []
68
69
  files:
@@ -70,6 +71,7 @@ files:
70
71
  - README.md
71
72
  - VERSION
72
73
  - exe/ruby-lsp
74
+ - exe/ruby-lsp-check
73
75
  - lib/rubocop/cop/ruby_lsp/use_language_server_aliases.rb
74
76
  - lib/ruby-lsp.rb
75
77
  - lib/ruby_lsp/check_docs.rb
@@ -84,6 +86,7 @@ files:
84
86
  - lib/ruby_lsp/requests/code_action_resolve.rb
85
87
  - lib/ruby_lsp/requests/code_actions.rb
86
88
  - lib/ruby_lsp/requests/code_lens.rb
89
+ - lib/ruby_lsp/requests/definition.rb
87
90
  - lib/ruby_lsp/requests/diagnostics.rb
88
91
  - lib/ruby_lsp/requests/document_highlight.rb
89
92
  - lib/ruby_lsp/requests/document_link.rb
@@ -96,6 +99,7 @@ files:
96
99
  - lib/ruby_lsp/requests/path_completion.rb
97
100
  - lib/ruby_lsp/requests/selection_ranges.rb
98
101
  - lib/ruby_lsp/requests/semantic_highlighting.rb
102
+ - lib/ruby_lsp/requests/show_syntax_tree.rb
99
103
  - lib/ruby_lsp/requests/support/annotation.rb
100
104
  - lib/ruby_lsp/requests/support/common.rb
101
105
  - lib/ruby_lsp/requests/support/dependency_detector.rb
@@ -113,6 +117,7 @@ files:
113
117
  - lib/ruby_lsp/requests/support/source_uri.rb
114
118
  - lib/ruby_lsp/requests/support/syntax_tree_formatting_runner.rb
115
119
  - lib/ruby_lsp/server.rb
120
+ - lib/ruby_lsp/setup_bundler.rb
116
121
  - lib/ruby_lsp/store.rb
117
122
  - lib/ruby_lsp/utils.rb
118
123
  homepage: https://github.com/Shopify/ruby-lsp
@@ -135,7 +140,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
135
140
  - !ruby/object:Gem::Version
136
141
  version: '0'
137
142
  requirements: []
138
- rubygems_version: 3.4.14
143
+ rubygems_version: 3.4.16
139
144
  signing_key:
140
145
  specification_version: 4
141
146
  summary: An opinionated language server for Ruby