ruby-lsp 0.8.0 → 0.9.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.
@@ -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