ruby-lsp 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -15,6 +15,7 @@ module RubyLsp
15
15
  @store = store
16
16
  @test_library = T.let(DependencyDetector.detected_test_library, String)
17
17
  @message_queue = message_queue
18
+ @index = T.let(RubyIndexer::Index.new, RubyIndexer::Index)
18
19
  end
19
20
 
20
21
  sig { params(request: T::Hash[Symbol, T.untyped]).returns(Result) }
@@ -57,6 +58,7 @@ module RubyLsp
57
58
  warn(errored_extensions.map(&:backtraces).join("\n\n"))
58
59
  end
59
60
 
61
+ perform_initial_indexing
60
62
  check_formatter_is_available
61
63
 
62
64
  warn("Ruby LSP is ready")
@@ -169,25 +171,91 @@ module RubyLsp
169
171
  completion(uri, request.dig(:params, :position))
170
172
  when "textDocument/definition"
171
173
  definition(uri, request.dig(:params, :position))
174
+ when "workspace/didChangeWatchedFiles"
175
+ did_change_watched_files(request.dig(:params, :changes))
176
+ when "workspace/symbol"
177
+ workspace_symbol(request.dig(:params, :query))
172
178
  when "rubyLsp/textDocument/showSyntaxTree"
173
179
  show_syntax_tree(uri, request.dig(:params, :range))
174
180
  end
175
181
  end
176
182
 
183
+ sig { params(changes: T::Array[{ uri: String, type: Integer }]).returns(Object) }
184
+ def did_change_watched_files(changes)
185
+ changes.each do |change|
186
+ # File change events include folders, but we're only interested in files
187
+ uri = URI(change[:uri])
188
+ file_path = uri.to_standardized_path
189
+ next if file_path.nil? || File.directory?(file_path)
190
+
191
+ case change[:type]
192
+ when Constant::FileChangeType::CREATED
193
+ @index.index_single(file_path)
194
+ when Constant::FileChangeType::CHANGED
195
+ @index.delete(file_path)
196
+ @index.index_single(file_path)
197
+ when Constant::FileChangeType::DELETED
198
+ @index.delete(file_path)
199
+ end
200
+ end
201
+
202
+ VOID
203
+ end
204
+
205
+ sig { void }
206
+ def perform_initial_indexing
207
+ return unless @store.experimental_features
208
+
209
+ # The begin progress invocation happens during `initialize`, so that the notification is sent before we are
210
+ # stuck indexing files
211
+ RubyIndexer.configuration.load_config
212
+
213
+ begin
214
+ @index.index_all
215
+ rescue StandardError => error
216
+ @message_queue << Notification.new(
217
+ message: "window/showMessage",
218
+ params: Interface::ShowMessageParams.new(
219
+ type: Constant::MessageType::ERROR,
220
+ message: "Error while indexing: #{error.message}",
221
+ ),
222
+ )
223
+ end
224
+
225
+ # Always end the progress notification even if indexing failed or else it never goes away and the user has no way
226
+ # of dismissing it
227
+ end_progress("indexing-progress")
228
+ end
229
+
230
+ sig { params(query: T.nilable(String)).returns(T::Array[Interface::WorkspaceSymbol]) }
231
+ def workspace_symbol(query)
232
+ Requests::WorkspaceSymbol.new(query, @index).run
233
+ end
234
+
177
235
  sig { params(uri: URI::Generic, range: T.nilable(Document::RangeShape)).returns({ ast: String }) }
178
236
  def show_syntax_tree(uri, range)
179
237
  { ast: Requests::ShowSyntaxTree.new(@store.get(uri), range).run }
180
238
  end
181
239
 
182
- sig { params(uri: URI::Generic, position: Document::PositionShape).returns(T.nilable(Interface::Location)) }
240
+ sig do
241
+ params(
242
+ uri: URI::Generic,
243
+ position: Document::PositionShape,
244
+ ).returns(T.nilable(T.any(T::Array[Interface::Location], Interface::Location)))
245
+ end
183
246
  def definition(uri, position)
184
247
  document = @store.get(uri)
185
248
  return if document.syntax_error?
186
249
 
187
- target, _parent = document.locate_node(position, node_types: [SyntaxTree::Command])
250
+ target, parent, nesting = document.locate_node(
251
+ position,
252
+ node_types: [SyntaxTree::Command, SyntaxTree::Const, SyntaxTree::ConstPathRef],
253
+ )
254
+
255
+ target = parent if target.is_a?(SyntaxTree::Const) && parent.is_a?(SyntaxTree::ConstPathRef)
188
256
 
189
257
  emitter = EventEmitter.new
190
- base_listener = Requests::Definition.new(uri, emitter, @message_queue)
258
+ base_listener = Requests::Definition.new(uri, nesting, @index, emitter, @message_queue)
191
259
  emitter.emit_for_target(target)
192
260
  base_listener.response
193
261
  end
@@ -209,16 +277,20 @@ module RubyLsp
209
277
  document = @store.get(uri)
210
278
  return if document.syntax_error?
211
279
 
212
- target, parent = document.locate_node(position)
280
+ target, parent, nesting = document.locate_node(
281
+ position,
282
+ node_types: Requests::Hover::ALLOWED_TARGETS,
283
+ )
213
284
 
214
- if !Requests::Hover::ALLOWED_TARGETS.include?(target.class) &&
215
- Requests::Hover::ALLOWED_TARGETS.include?(parent.class)
285
+ if (Requests::Hover::ALLOWED_TARGETS.include?(parent.class) &&
286
+ !Requests::Hover::ALLOWED_TARGETS.include?(target.class)) ||
287
+ (parent.is_a?(SyntaxTree::ConstPathRef) && target.is_a?(SyntaxTree::Const))
216
288
  target = parent
217
289
  end
218
290
 
219
291
  # Instantiate all listeners
220
292
  emitter = EventEmitter.new
221
- hover = Requests::Hover.new(emitter, @message_queue)
293
+ hover = Requests::Hover.new(@index, nesting, emitter, @message_queue)
222
294
 
223
295
  # Emit events for all listeners
224
296
  emitter.emit_for_target(target)
@@ -436,6 +508,37 @@ module RubyLsp
436
508
  listener.response
437
509
  end
438
510
 
511
+ sig { params(id: String, title: String).void }
512
+ def begin_progress(id, title)
513
+ return unless @store.supports_progress
514
+
515
+ @message_queue << Request.new(
516
+ message: "window/workDoneProgress/create",
517
+ params: Interface::WorkDoneProgressCreateParams.new(token: id),
518
+ )
519
+
520
+ @message_queue << Notification.new(
521
+ message: "$/progress",
522
+ params: Interface::ProgressParams.new(
523
+ token: id,
524
+ value: Interface::WorkDoneProgressBegin.new(kind: "begin", title: title),
525
+ ),
526
+ )
527
+ end
528
+
529
+ sig { params(id: String).void }
530
+ def end_progress(id)
531
+ return unless @store.supports_progress
532
+
533
+ @message_queue << Notification.new(
534
+ message: "$/progress",
535
+ params: Interface::ProgressParams.new(
536
+ token: id,
537
+ value: Interface::WorkDoneProgressEnd.new(kind: "end"),
538
+ ),
539
+ )
540
+ end
541
+
439
542
  sig { params(options: T::Hash[Symbol, T.untyped]).returns(Interface::InitializeResult) }
440
543
  def initialize_request(options)
441
544
  @store.clear
@@ -449,6 +552,7 @@ module RubyLsp
449
552
  encodings.first
450
553
  end
451
554
 
555
+ @store.supports_progress = options.dig(:capabilities, :window, :workDoneProgress) || true
452
556
  formatter = options.dig(:initializationOptions, :formatter) || "auto"
453
557
  @store.formatter = if formatter == "auto"
454
558
  DependencyDetector.detected_formatter
@@ -457,9 +561,7 @@ module RubyLsp
457
561
  end
458
562
 
459
563
  configured_features = options.dig(:initializationOptions, :enabledFeatures)
460
-
461
- # Uncomment the line below and use the variable to gate features behind the experimental flag
462
- # experimental_features = options.dig(:initializationOptions, :experimentalFeaturesEnabled)
564
+ @store.experimental_features = options.dig(:initializationOptions, :experimentalFeaturesEnabled) || false
463
565
 
464
566
  enabled_features = case configured_features
465
567
  when Array
@@ -541,6 +643,37 @@ module RubyLsp
541
643
  )
542
644
  end
543
645
 
646
+ if @store.experimental_features
647
+ # Dynamically registered capabilities
648
+ file_watching_caps = options.dig(:capabilities, :workspace, :didChangeWatchedFiles)
649
+
650
+ # Not every client supports dynamic registration or file watching
651
+ if file_watching_caps&.dig(:dynamicRegistration) && file_watching_caps&.dig(:relativePatternSupport)
652
+ @message_queue << Request.new(
653
+ message: "client/registerCapability",
654
+ params: Interface::RegistrationParams.new(
655
+ registrations: [
656
+ # Register watching Ruby files
657
+ Interface::Registration.new(
658
+ id: "workspace/didChangeWatchedFiles",
659
+ method: "workspace/didChangeWatchedFiles",
660
+ register_options: Interface::DidChangeWatchedFilesRegistrationOptions.new(
661
+ watchers: [
662
+ Interface::FileSystemWatcher.new(
663
+ glob_pattern: "**/*.rb",
664
+ kind: Constant::WatchKind::CREATE | Constant::WatchKind::CHANGE | Constant::WatchKind::DELETE,
665
+ ),
666
+ ],
667
+ ),
668
+ ),
669
+ ],
670
+ ),
671
+ )
672
+ end
673
+
674
+ begin_progress("indexing-progress", "Ruby LSP: indexing files")
675
+ end
676
+
544
677
  Interface::InitializeResult.new(
545
678
  capabilities: Interface::ServerCapabilities.new(
546
679
  text_document_sync: Interface::TextDocumentSyncOptions.new(
@@ -563,6 +696,7 @@ module RubyLsp
563
696
  completion_provider: completion_provider,
564
697
  code_lens_provider: code_lens_provider,
565
698
  definition_provider: enabled_features["definition"],
699
+ workspace_symbol_provider: enabled_features["workspaceSymbol"],
566
700
  ),
567
701
  )
568
702
  end
@@ -3,6 +3,7 @@
3
3
 
4
4
  require "sorbet-runtime"
5
5
  require "syntax_tree"
6
+ require "yarp"
6
7
  require "language_server-protocol"
7
8
  require "benchmark"
8
9
  require "bundler"
@@ -10,6 +11,7 @@ require "uri"
10
11
  require "cgi"
11
12
 
12
13
  require "ruby-lsp"
14
+ require "ruby_indexer/ruby_indexer"
13
15
  require "core_ext/uri"
14
16
  require "ruby_lsp/utils"
15
17
  require "ruby_lsp/server"
@@ -9,29 +9,51 @@ module RubyLsp
9
9
  # request](https://microsoft.github.io/language-server-protocol/specification#textDocument_definition) jumps to the
10
10
  # definition of the symbol under the cursor.
11
11
  #
12
- # Currently, only jumping to required files is supported.
12
+ # Currently, only jumping to classes, modules and required files is supported.
13
13
  #
14
14
  # # Example
15
15
  #
16
16
  # ```ruby
17
17
  # require "some_gem/file" # <- Request go to definition on this string will take you to the file
18
+ # Product.new # <- Request go to definition on this class name will take you to its declaration.
18
19
  # ```
19
20
  class Definition < Listener
20
21
  extend T::Sig
21
22
  extend T::Generic
22
23
 
23
- ResponseType = type_member { { fixed: T.nilable(Interface::Location) } }
24
+ ResponseType = type_member { { fixed: T.nilable(T.any(T::Array[Interface::Location], Interface::Location)) } }
24
25
 
25
26
  sig { override.returns(ResponseType) }
26
27
  attr_reader :response
27
28
 
28
- sig { params(uri: URI::Generic, emitter: EventEmitter, message_queue: Thread::Queue).void }
29
- def initialize(uri, emitter, message_queue)
29
+ sig do
30
+ params(
31
+ uri: URI::Generic,
32
+ nesting: T::Array[String],
33
+ index: RubyIndexer::Index,
34
+ emitter: EventEmitter,
35
+ message_queue: Thread::Queue,
36
+ ).void
37
+ end
38
+ def initialize(uri, nesting, index, emitter, message_queue)
30
39
  super(emitter, message_queue)
31
40
 
32
41
  @uri = uri
42
+ @nesting = nesting
43
+ @index = index
33
44
  @response = T.let(nil, ResponseType)
34
- emitter.register(self, :on_command)
45
+ emitter.register(self, :on_command, :on_const, :on_const_path_ref)
46
+ end
47
+
48
+ sig { params(node: SyntaxTree::ConstPathRef).void }
49
+ def on_const_path_ref(node)
50
+ name = full_constant_name(node)
51
+ find_in_index(name)
52
+ end
53
+
54
+ sig { params(node: SyntaxTree::Const).void }
55
+ def on_const(node)
56
+ find_in_index(node.value)
35
57
  end
36
58
 
37
59
  sig { params(node: SyntaxTree::Command).void }
@@ -79,6 +101,39 @@ module RubyLsp
79
101
 
80
102
  private
81
103
 
104
+ sig { params(value: String).void }
105
+ def find_in_index(value)
106
+ entries = @index.resolve(value, @nesting)
107
+ return unless entries
108
+
109
+ bundle_path = begin
110
+ Bundler.bundle_path.to_s
111
+ rescue Bundler::GemfileNotFound
112
+ nil
113
+ end
114
+
115
+ @response = entries.filter_map do |entry|
116
+ location = entry.location
117
+ # If the project has Sorbet, then we only want to handle go to definition for constants defined in gems, as an
118
+ # additional behavior on top of jumping to RBIs. Sorbet can already handle go to definition for all constants
119
+ # in the project, even if the files are typed false
120
+ file_path = entry.file_path
121
+ if DependencyDetector::HAS_TYPECHECKER && bundle_path && !file_path.start_with?(bundle_path) &&
122
+ !file_path.start_with?(RbConfig::CONFIG["rubylibdir"])
123
+
124
+ next
125
+ end
126
+
127
+ Interface::Location.new(
128
+ uri: URI::Generic.from_path(path: file_path).to_s,
129
+ range: Interface::Range.new(
130
+ start: Interface::Position.new(line: location.start_line - 1, character: location.start_column),
131
+ end: Interface::Position.new(line: location.end_line - 1, character: location.end_column),
132
+ ),
133
+ )
134
+ end
135
+ end
136
+
82
137
  sig { params(file: String).returns(T.nilable(String)) }
83
138
  def find_file_in_load_path(file)
84
139
  return unless file.include?("/")
@@ -3,19 +3,15 @@
3
3
 
4
4
  module RubyLsp
5
5
  module Requests
6
- # ![Hover demo](../../rails_document_link_hover.gif)
6
+ # ![Hover demo](../../hover.gif)
7
7
  #
8
8
  # The [hover request](https://microsoft.github.io/language-server-protocol/specification#textDocument_hover)
9
- # renders a clickable link to the code's official documentation.
10
- # It currently only supports Rails' documentation: when hovering over Rails DSLs/constants under certain paths,
11
- # like `before_save :callback` in `models/post.rb`, it generates a link to `before_save`'s API documentation.
9
+ # displays the documentation for the symbol currently under the cursor.
12
10
  #
13
11
  # # Example
14
12
  #
15
13
  # ```ruby
16
- # class Post < ApplicationRecord
17
- # before_save :do_something # when hovering on before_save, the link will be rendered
18
- # end
14
+ # String # -> Hovering over the class reference will show all declaration locations and the documentation
19
15
  # ```
20
16
  class Hover < Listener
21
17
  extend T::Sig
@@ -25,6 +21,7 @@ module RubyLsp
25
21
 
26
22
  ALLOWED_TARGETS = T.let(
27
23
  [
24
+ SyntaxTree::Const,
28
25
  SyntaxTree::Command,
29
26
  SyntaxTree::CallNode,
30
27
  SyntaxTree::ConstPathRef,
@@ -35,15 +32,24 @@ module RubyLsp
35
32
  sig { override.returns(ResponseType) }
36
33
  attr_reader :response
37
34
 
38
- sig { params(emitter: EventEmitter, message_queue: Thread::Queue).void }
39
- def initialize(emitter, message_queue)
40
- super
35
+ sig do
36
+ params(
37
+ index: RubyIndexer::Index,
38
+ nesting: T::Array[String],
39
+ emitter: EventEmitter,
40
+ message_queue: Thread::Queue,
41
+ ).void
42
+ end
43
+ def initialize(index, nesting, emitter, message_queue)
44
+ super(emitter, message_queue)
41
45
 
46
+ @nesting = nesting
47
+ @index = index
42
48
  @external_listeners.concat(
43
49
  Extension.extensions.filter_map { |ext| ext.create_hover_listener(emitter, message_queue) },
44
50
  )
45
51
  @response = T.let(nil, ResponseType)
46
- emitter.register(self, :on_command, :on_const_path_ref, :on_call)
52
+ emitter.register(self, :on_const_path_ref, :on_const)
47
53
  end
48
54
 
49
55
  # Merges responses from other hover listeners
@@ -55,40 +61,57 @@ module RubyLsp
55
61
  if @response.nil?
56
62
  @response = other.response
57
63
  else
58
- @response.contents.value << other_response.contents.value << "\n\n"
64
+ @response.contents.value << "\n\n" << other_response.contents.value
59
65
  end
60
66
 
61
67
  self
62
68
  end
63
69
 
64
- sig { params(node: SyntaxTree::Command).void }
65
- def on_command(node)
66
- message = node.message
67
- @response = generate_rails_document_link_hover(message.value, message)
68
- end
69
-
70
70
  sig { params(node: SyntaxTree::ConstPathRef).void }
71
71
  def on_const_path_ref(node)
72
- @response = generate_rails_document_link_hover(full_constant_name(node), node)
72
+ return if DependencyDetector::HAS_TYPECHECKER
73
+
74
+ name = full_constant_name(node)
75
+ generate_hover(name, node)
73
76
  end
74
77
 
75
- sig { params(node: SyntaxTree::CallNode).void }
76
- def on_call(node)
77
- message = node.message
78
- return if message.is_a?(Symbol)
78
+ sig { params(node: SyntaxTree::Const).void }
79
+ def on_const(node)
80
+ return if DependencyDetector::HAS_TYPECHECKER
79
81
 
80
- @response = generate_rails_document_link_hover(message.value, message)
82
+ generate_hover(node.value, node)
81
83
  end
82
84
 
83
85
  private
84
86
 
85
- sig { params(name: String, node: SyntaxTree::Node).returns(T.nilable(Interface::Hover)) }
86
- def generate_rails_document_link_hover(name, node)
87
- urls = Support::RailsDocumentClient.generate_rails_document_urls(name)
88
- return if urls.empty?
87
+ sig { params(name: String, node: SyntaxTree::Node).void }
88
+ def generate_hover(name, node)
89
+ entries = @index.resolve(name, @nesting)
90
+ return unless entries
91
+
92
+ title = +"```ruby\n#{name}\n```"
93
+ definitions = []
94
+ content = +""
95
+ entries.each do |entry|
96
+ loc = entry.location
97
+
98
+ # We always handle locations as zero based. However, for file links in Markdown we need them to be one based,
99
+ # which is why instead of the usual subtraction of 1 to line numbers, we are actually adding 1 to columns. The
100
+ # format for VS Code file URIs is `file:///path/to/file.rb#Lstart_line,start_column-end_line,end_column`
101
+ uri = URI::Generic.from_path(
102
+ path: entry.file_path,
103
+ fragment: "L#{loc.start_line},#{loc.start_column + 1}-#{loc.end_line},#{loc.end_column + 1}",
104
+ )
105
+
106
+ definitions << "[#{entry.file_name}](#{uri})"
107
+ content << "\n\n#{entry.comments.join("\n")}" unless entry.comments.empty?
108
+ end
89
109
 
90
- contents = Interface::MarkupContent.new(kind: "markdown", value: urls.join("\n\n"))
91
- Interface::Hover.new(range: range_from_syntax_tree_node(node), contents: contents)
110
+ contents = Interface::MarkupContent.new(
111
+ kind: "markdown",
112
+ value: "#{title}\n\n**Definitions**: #{definitions.join(" | ")}\n\n#{content}",
113
+ )
114
+ @response = Interface::Hover.new(range: range_from_syntax_tree_node(node), contents: contents)
92
115
  end
93
116
  end
94
117
  end
@@ -67,12 +67,10 @@ module RubyLsp
67
67
 
68
68
  line = T.must(current_line)
69
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
-
70
+ # If the user inserts the closing pipe manually to the end of the block argument, we need to avoid adding
71
+ # an additional one and remove the previous one. This also helps to remove the user who accidentally
72
+ # inserts another pipe after the autocompleted one.
73
+ if line[..@position[:character]] =~ /(do|{)\s+\|[^|]*\|\|$/
76
74
  @edits << Interface::TextEdit.new(
77
75
  range: Interface::Range.new(
78
76
  start: Interface::Position.new(
@@ -36,6 +36,11 @@ module RubyLsp
36
36
  end
37
37
  end
38
38
 
39
+ sig { returns(T::Boolean) }
40
+ def typechecker?
41
+ direct_dependency?(/^sorbet/) || direct_dependency?(/^sorbet-static-and-runtime/)
42
+ end
43
+
39
44
  sig { params(gem_pattern: Regexp).returns(T::Boolean) }
40
45
  def direct_dependency?(gem_pattern)
41
46
  Bundler.with_original_env { Bundler.default_gemfile } &&
@@ -44,5 +49,7 @@ module RubyLsp
44
49
  false
45
50
  end
46
51
  end
52
+
53
+ HAS_TYPECHECKER = T.let(typechecker?, T::Boolean)
47
54
  end
48
55
  end
@@ -0,0 +1,86 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ module Requests
6
+ # ![Workspace symbol demo](../../workspace_symbol.gif)
7
+ #
8
+ # The [workspace symbol](https://microsoft.github.io/language-server-protocol/specification#workspace_symbol)
9
+ # request allows fuzzy searching declarations in the entire project. On VS Code, use CTRL/CMD + T to search for
10
+ # symbols.
11
+ #
12
+ # # Example
13
+ #
14
+ # ```ruby
15
+ # # Searching for `Floo` will fuzzy match and return all declarations according to the query, including this `Foo`
16
+ # class
17
+ # class Foo
18
+ # end
19
+ # ```
20
+ #
21
+ class WorkspaceSymbol
22
+ extend T::Sig
23
+
24
+ sig { params(query: T.nilable(String), index: RubyIndexer::Index).void }
25
+ def initialize(query, index)
26
+ @query = query
27
+ @index = index
28
+ end
29
+
30
+ sig { returns(T::Array[Interface::WorkspaceSymbol]) }
31
+ def run
32
+ bundle_path = begin
33
+ Bundler.bundle_path.to_s
34
+ rescue Bundler::GemfileNotFound
35
+ nil
36
+ end
37
+
38
+ @index.fuzzy_search(@query).filter_map do |entry|
39
+ # If the project is using Sorbet, we let Sorbet handle symbols defined inside the project itself and RBIs, but
40
+ # we still return entries defined in gems to allow developers to jump directly to the source
41
+ file_path = entry.file_path
42
+ if DependencyDetector::HAS_TYPECHECKER && bundle_path && !file_path.start_with?(bundle_path) &&
43
+ !file_path.start_with?(RbConfig::CONFIG["rubylibdir"])
44
+
45
+ next
46
+ end
47
+
48
+ kind = kind_for_entry(entry)
49
+ loc = entry.location
50
+
51
+ # We use the namespace as the container name, but we also use the full name as the regular name. The reason we
52
+ # do this is to allow people to search for fully qualified names (e.g.: `Foo::Bar`). If we only included the
53
+ # short name `Bar`, then searching for `Foo::Bar` would not return any results
54
+ *container, _short_name = entry.name.split("::")
55
+
56
+ Interface::WorkspaceSymbol.new(
57
+ name: entry.name,
58
+ container_name: T.must(container).join("::"),
59
+ kind: kind,
60
+ location: Interface::Location.new(
61
+ uri: URI::Generic.from_path(path: file_path).to_s,
62
+ range: Interface::Range.new(
63
+ start: Interface::Position.new(line: loc.start_line - 1, character: loc.start_column),
64
+ end: Interface::Position.new(line: loc.end_line - 1, character: loc.end_column),
65
+ ),
66
+ ),
67
+ )
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ sig { params(entry: RubyIndexer::Index::Entry).returns(T.nilable(Integer)) }
74
+ def kind_for_entry(entry)
75
+ case entry
76
+ when RubyIndexer::Index::Entry::Class
77
+ Constant::SymbolKind::CLASS
78
+ when RubyIndexer::Index::Entry::Module
79
+ Constant::SymbolKind::NAMESPACE
80
+ when RubyIndexer::Index::Entry::Constant
81
+ Constant::SymbolKind::CONSTANT
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -41,6 +41,7 @@ module RubyLsp
41
41
  autoload :CodeLens, "ruby_lsp/requests/code_lens"
42
42
  autoload :Definition, "ruby_lsp/requests/definition"
43
43
  autoload :ShowSyntaxTree, "ruby_lsp/requests/show_syntax_tree"
44
+ autoload :WorkspaceSymbol, "ruby_lsp/requests/workspace_symbol"
44
45
 
45
46
  # :nodoc:
46
47
  module Support