ruby-lsp 0.6.1 → 0.7.0

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: 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