ruby-lsp 0.23.1 → 0.23.5

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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/exe/ruby-lsp-launcher +18 -9
  4. data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +89 -58
  5. data/lib/ruby_indexer/lib/ruby_indexer/index.rb +21 -10
  6. data/lib/ruby_indexer/lib/ruby_indexer/visibility_scope.rb +36 -0
  7. data/lib/ruby_indexer/ruby_indexer.rb +1 -0
  8. data/lib/ruby_indexer/test/index_test.rb +2 -1
  9. data/lib/ruby_indexer/test/instance_variables_test.rb +20 -0
  10. data/lib/ruby_indexer/test/method_test.rb +86 -8
  11. data/lib/ruby_lsp/base_server.rb +11 -21
  12. data/lib/ruby_lsp/document.rb +62 -9
  13. data/lib/ruby_lsp/erb_document.rb +5 -3
  14. data/lib/ruby_lsp/global_state.rb +9 -0
  15. data/lib/ruby_lsp/listeners/definition.rb +7 -2
  16. data/lib/ruby_lsp/rbs_document.rb +2 -2
  17. data/lib/ruby_lsp/requests/code_action_resolve.rb +2 -6
  18. data/lib/ruby_lsp/requests/completion.rb +2 -1
  19. data/lib/ruby_lsp/requests/definition.rb +1 -1
  20. data/lib/ruby_lsp/requests/document_highlight.rb +1 -1
  21. data/lib/ruby_lsp/requests/hover.rb +1 -1
  22. data/lib/ruby_lsp/requests/prepare_rename.rb +1 -1
  23. data/lib/ruby_lsp/requests/references.rb +1 -1
  24. data/lib/ruby_lsp/requests/rename.rb +3 -3
  25. data/lib/ruby_lsp/requests/show_syntax_tree.rb +1 -4
  26. data/lib/ruby_lsp/requests/signature_help.rb +1 -1
  27. data/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb +3 -3
  28. data/lib/ruby_lsp/requests/support/rubocop_runner.rb +13 -13
  29. data/lib/ruby_lsp/requests/workspace_symbol.rb +2 -2
  30. data/lib/ruby_lsp/ruby_document.rb +75 -6
  31. data/lib/ruby_lsp/server.rb +69 -49
  32. data/lib/ruby_lsp/store.rb +7 -7
  33. data/lib/ruby_lsp/type_inferrer.rb +39 -17
  34. data/lib/ruby_lsp/utils.rb +43 -0
  35. metadata +3 -2
@@ -18,22 +18,21 @@ module RubyLsp
18
18
  @incoming_queue = T.let(Thread::Queue.new, Thread::Queue)
19
19
  @outgoing_queue = T.let(Thread::Queue.new, Thread::Queue)
20
20
  @cancelled_requests = T.let([], T::Array[Integer])
21
- @mutex = T.let(Mutex.new, Mutex)
22
21
  @worker = T.let(new_worker, Thread)
23
22
  @current_request_id = T.let(1, Integer)
24
- @store = T.let(Store.new, Store)
23
+ @global_state = T.let(GlobalState.new, GlobalState)
24
+ @store = T.let(Store.new(@global_state), Store)
25
25
  @outgoing_dispatcher = T.let(
26
26
  Thread.new do
27
27
  unless @test_mode
28
28
  while (message = @outgoing_queue.pop)
29
- @mutex.synchronize { @writer.write(message.to_hash) }
29
+ @global_state.synchronize { @writer.write(message.to_hash) }
30
30
  end
31
31
  end
32
32
  end,
33
33
  Thread,
34
34
  )
35
35
 
36
- @global_state = T.let(GlobalState.new, GlobalState)
37
36
  Thread.main.priority = 1
38
37
 
39
38
  # We read the initialize request in `exe/ruby-lsp` to be able to determine the workspace URI where Bundler should
@@ -51,7 +50,7 @@ module RubyLsp
51
50
  # source. Altering the source reference during parsing will put the parser in an invalid internal state, since
52
51
  # it started parsing with one source but then it changed in the middle. We don't want to do this for text
53
52
  # synchronization notifications
54
- @mutex.synchronize do
53
+ @global_state.synchronize do
55
54
  uri = message.dig(:params, :textDocument, :uri)
56
55
 
57
56
  if uri
@@ -91,22 +90,17 @@ module RubyLsp
91
90
  # The following requests need to be executed in the main thread directly to avoid concurrency issues. Everything
92
91
  # else is pushed into the incoming queue
93
92
  case method
94
- when "initialize", "initialized", "textDocument/didOpen", "textDocument/didClose", "textDocument/didChange",
95
- "$/cancelRequest"
93
+ when "initialize", "initialized", "textDocument/didOpen", "textDocument/didClose", "textDocument/didChange"
96
94
  process_message(message)
97
95
  when "shutdown"
98
- @mutex.synchronize do
96
+ @global_state.synchronize do
99
97
  send_log_message("Shutting down Ruby LSP...")
100
98
  shutdown
101
99
  run_shutdown
102
100
  @writer.write(Result.new(id: message[:id], response: nil).to_hash)
103
101
  end
104
102
  when "exit"
105
- @mutex.synchronize do
106
- status = @incoming_queue.closed? ? 0 : 1
107
- send_log_message("Shutdown complete with status #{status}")
108
- exit(status)
109
- end
103
+ @global_state.synchronize { exit(@incoming_queue.closed? ? 0 : 1) }
110
104
  else
111
105
  @incoming_queue << message
112
106
  end
@@ -121,8 +115,8 @@ module RubyLsp
121
115
  @outgoing_queue.close
122
116
  @cancelled_requests.clear
123
117
 
124
- @worker.join
125
- @outgoing_dispatcher.join
118
+ @worker.terminate
119
+ @outgoing_dispatcher.terminate
126
120
  @store.clear
127
121
  end
128
122
 
@@ -157,13 +151,9 @@ module RubyLsp
157
151
  id = message[:id]
158
152
 
159
153
  # Check if the request was cancelled before trying to process it
160
- @mutex.synchronize do
154
+ @global_state.synchronize do
161
155
  if id && @cancelled_requests.include?(id)
162
- send_message(Error.new(
163
- id: id,
164
- code: Constant::ErrorCodes::REQUEST_CANCELLED,
165
- message: "Request #{id} was cancelled",
166
- ))
156
+ send_message(Result.new(id: id, response: nil))
167
157
  @cancelled_requests.delete(id)
168
158
  next
169
159
  end
@@ -40,19 +40,24 @@ module RubyLsp
40
40
  sig { returns(Encoding) }
41
41
  attr_reader :encoding
42
42
 
43
+ sig { returns(T.nilable(Edit)) }
44
+ attr_reader :last_edit
45
+
43
46
  sig { returns(T.any(Interface::SemanticTokens, Object)) }
44
47
  attr_accessor :semantic_tokens
45
48
 
46
- sig { params(source: String, version: Integer, uri: URI::Generic, encoding: Encoding).void }
47
- def initialize(source:, version:, uri:, encoding: Encoding::UTF_8)
49
+ sig { params(source: String, version: Integer, uri: URI::Generic, global_state: GlobalState).void }
50
+ def initialize(source:, version:, uri:, global_state:)
51
+ @source = source
52
+ @version = version
53
+ @global_state = global_state
48
54
  @cache = T.let(Hash.new(EMPTY_CACHE), T::Hash[String, T.untyped])
49
55
  @semantic_tokens = T.let(EMPTY_CACHE, T.any(Interface::SemanticTokens, Object))
50
- @encoding = T.let(encoding, Encoding)
51
- @source = T.let(source, String)
52
- @version = T.let(version, Integer)
56
+ @encoding = T.let(global_state.encoding, Encoding)
53
57
  @uri = T.let(uri, URI::Generic)
54
58
  @needs_parsing = T.let(true, T::Boolean)
55
59
  @parse_result = T.let(T.unsafe(nil), ParseResultType)
60
+ @last_edit = T.let(nil, T.nilable(Edit))
56
61
  parse!
57
62
  end
58
63
 
@@ -64,7 +69,6 @@ module RubyLsp
64
69
  sig { abstract.returns(LanguageId) }
65
70
  def language_id; end
66
71
 
67
- # TODO: remove this method once all non-positional requests have been migrated to the listener pattern
68
72
  sig do
69
73
  type_parameters(:T)
70
74
  .params(
@@ -106,6 +110,19 @@ module RubyLsp
106
110
  @version = version
107
111
  @needs_parsing = true
108
112
  @cache.clear
113
+
114
+ last_edit = edits.last
115
+ return unless last_edit
116
+
117
+ last_edit_range = last_edit[:range]
118
+
119
+ @last_edit = if last_edit_range[:start] == last_edit_range[:end]
120
+ Insert.new(last_edit_range)
121
+ elsif last_edit[:text].empty?
122
+ Delete.new(last_edit_range)
123
+ else
124
+ Replace.new(last_edit_range)
125
+ end
109
126
  end
110
127
 
111
128
  # Returns `true` if the document was parsed and `false` if nothing needed parsing
@@ -115,16 +132,52 @@ module RubyLsp
115
132
  sig { abstract.returns(T::Boolean) }
116
133
  def syntax_error?; end
117
134
 
135
+ sig { returns(T::Boolean) }
136
+ def past_expensive_limit?
137
+ @source.length > MAXIMUM_CHARACTERS_FOR_EXPENSIVE_FEATURES
138
+ end
139
+
140
+ sig do
141
+ params(
142
+ start_pos: T::Hash[Symbol, T.untyped],
143
+ end_pos: T.nilable(T::Hash[Symbol, T.untyped]),
144
+ ).returns([Integer, T.nilable(Integer)])
145
+ end
146
+ def find_index_by_position(start_pos, end_pos = nil)
147
+ @global_state.synchronize do
148
+ scanner = create_scanner
149
+ start_index = scanner.find_char_position(start_pos)
150
+ end_index = scanner.find_char_position(end_pos) if end_pos
151
+ [start_index, end_index]
152
+ end
153
+ end
154
+
155
+ private
156
+
118
157
  sig { returns(Scanner) }
119
158
  def create_scanner
120
159
  Scanner.new(@source, @encoding)
121
160
  end
122
161
 
123
- sig { returns(T::Boolean) }
124
- def past_expensive_limit?
125
- @source.length > MAXIMUM_CHARACTERS_FOR_EXPENSIVE_FEATURES
162
+ class Edit
163
+ extend T::Sig
164
+ extend T::Helpers
165
+
166
+ abstract!
167
+
168
+ sig { returns(T::Hash[Symbol, T.untyped]) }
169
+ attr_reader :range
170
+
171
+ sig { params(range: T::Hash[Symbol, T.untyped]).void }
172
+ def initialize(range)
173
+ @range = range
174
+ end
126
175
  end
127
176
 
177
+ class Insert < Edit; end
178
+ class Replace < Edit; end
179
+ class Delete < Edit; end
180
+
128
181
  class Scanner
129
182
  extend T::Sig
130
183
 
@@ -19,8 +19,8 @@ module RubyLsp
19
19
  end
20
20
  attr_reader :code_units_cache
21
21
 
22
- sig { params(source: String, version: Integer, uri: URI::Generic, encoding: Encoding).void }
23
- def initialize(source:, version:, uri:, encoding: Encoding::UTF_8)
22
+ sig { params(source: String, version: Integer, uri: URI::Generic, global_state: GlobalState).void }
23
+ def initialize(source:, version:, uri:, global_state:)
24
24
  # This has to be initialized before calling super because we call `parse` in the parent constructor, which
25
25
  # overrides this with the proper virtual host language source
26
26
  @host_language_source = T.let("", String)
@@ -63,9 +63,11 @@ module RubyLsp
63
63
  ).returns(NodeContext)
64
64
  end
65
65
  def locate_node(position, node_types: [])
66
+ char_position, _ = find_index_by_position(position)
67
+
66
68
  RubyDocument.locate(
67
69
  @parse_result.value,
68
- create_scanner.find_char_position(position),
70
+ char_position,
69
71
  code_units_cache: @code_units_cache,
70
72
  node_types: node_types,
71
73
  )
@@ -29,6 +29,9 @@ module RubyLsp
29
29
  sig { returns(ClientCapabilities) }
30
30
  attr_reader :client_capabilities
31
31
 
32
+ sig { returns(URI::Generic) }
33
+ attr_reader :workspace_uri
34
+
32
35
  sig { void }
33
36
  def initialize
34
37
  @workspace_uri = T.let(URI::Generic.from_path(path: Dir.pwd), URI::Generic)
@@ -53,6 +56,12 @@ module RubyLsp
53
56
  )
54
57
  @client_capabilities = T.let(ClientCapabilities.new, ClientCapabilities)
55
58
  @enabled_feature_flags = T.let({}, T::Hash[Symbol, T::Boolean])
59
+ @mutex = T.let(Mutex.new, Mutex)
60
+ end
61
+
62
+ sig { type_parameters(:T).params(block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T)) }
63
+ def synchronize(&block)
64
+ @mutex.synchronize(&block)
56
65
  end
57
66
 
58
67
  sig { params(addon_name: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
@@ -330,7 +330,8 @@ module RubyLsp
330
330
 
331
331
  methods.each do |target_method|
332
332
  uri = target_method.uri
333
- next if sorbet_level_true_or_higher?(@sorbet_level) && not_in_dependencies?(T.must(uri.full_path))
333
+ full_path = uri.full_path
334
+ next if sorbet_level_true_or_higher?(@sorbet_level) && (!full_path || not_in_dependencies?(full_path))
334
335
 
335
336
  @response_builder << Interface::LocationLink.new(
336
337
  target_uri: uri.to_s,
@@ -403,7 +404,11 @@ module RubyLsp
403
404
  # additional behavior on top of jumping to RBIs. The only sigil where Sorbet cannot handle constants is typed
404
405
  # ignore
405
406
  uri = entry.uri
406
- next if @sorbet_level != RubyDocument::SorbetLevel::Ignore && not_in_dependencies?(T.must(uri.full_path))
407
+ full_path = uri.full_path
408
+
409
+ if @sorbet_level != RubyDocument::SorbetLevel::Ignore && (!full_path || not_in_dependencies?(full_path))
410
+ next
411
+ end
407
412
 
408
413
  @response_builder << Interface::LocationLink.new(
409
414
  target_uri: uri.to_s,
@@ -8,8 +8,8 @@ module RubyLsp
8
8
 
9
9
  ParseResultType = type_member { { fixed: T::Array[RBS::AST::Declarations::Base] } }
10
10
 
11
- sig { params(source: String, version: Integer, uri: URI::Generic, encoding: Encoding).void }
12
- def initialize(source:, version:, uri:, encoding: Encoding::UTF_8)
11
+ sig { params(source: String, version: Integer, uri: URI::Generic, global_state: GlobalState).void }
12
+ def initialize(source:, version:, uri:, global_state:)
13
13
  @syntax_error = T.let(false, T::Boolean)
14
14
  super
15
15
  end
@@ -92,9 +92,7 @@ module RubyLsp
92
92
  source_range = @code_action.dig(:data, :range)
93
93
  return Error::EmptySelection if source_range[:start] == source_range[:end]
94
94
 
95
- scanner = @document.create_scanner
96
- start_index = scanner.find_char_position(source_range[:start])
97
- end_index = scanner.find_char_position(source_range[:end])
95
+ start_index, end_index = @document.find_index_by_position(source_range[:start], source_range[:end])
98
96
  extracted_source = T.must(@document.source[start_index...end_index])
99
97
 
100
98
  # Find the closest statements node, so that we place the refactor in a valid position
@@ -192,9 +190,7 @@ module RubyLsp
192
190
  source_range = @code_action.dig(:data, :range)
193
191
  return Error::EmptySelection if source_range[:start] == source_range[:end]
194
192
 
195
- scanner = @document.create_scanner
196
- start_index = scanner.find_char_position(source_range[:start])
197
- end_index = scanner.find_char_position(source_range[:end])
193
+ start_index, end_index = @document.find_index_by_position(source_range[:start], source_range[:end])
198
194
  extracted_source = T.must(@document.source[start_index...end_index])
199
195
 
200
196
  # Find the closest method declaration node, so that we place the refactor in a valid position
@@ -40,7 +40,8 @@ module RubyLsp
40
40
  @dispatcher = dispatcher
41
41
  # Completion always receives the position immediately after the character that was just typed. Here we adjust it
42
42
  # back by 1, so that we find the right node
43
- char_position = document.create_scanner.find_char_position(params[:position]) - 1
43
+ char_position, _ = document.find_index_by_position(params[:position])
44
+ char_position -= 1
44
45
  delegate_request_if_needed!(global_state, document, char_position)
45
46
 
46
47
  node_context = RubyDocument.locate(
@@ -29,7 +29,7 @@ module RubyLsp
29
29
  )
30
30
  @dispatcher = dispatcher
31
31
 
32
- char_position = document.create_scanner.find_char_position(position)
32
+ char_position, _ = document.find_index_by_position(position)
33
33
  delegate_request_if_needed!(global_state, document, char_position)
34
34
 
35
35
  node_context = RubyDocument.locate(
@@ -25,7 +25,7 @@ module RubyLsp
25
25
  end
26
26
  def initialize(global_state, document, position, dispatcher)
27
27
  super()
28
- char_position = document.create_scanner.find_char_position(position)
28
+ char_position, _ = document.find_index_by_position(position)
29
29
  delegate_request_if_needed!(global_state, document, char_position)
30
30
 
31
31
  node_context = RubyDocument.locate(
@@ -34,7 +34,7 @@ module RubyLsp
34
34
  def initialize(document, global_state, position, dispatcher, sorbet_level)
35
35
  super()
36
36
 
37
- char_position = document.create_scanner.find_char_position(position)
37
+ char_position, _ = document.find_index_by_position(position)
38
38
  delegate_request_if_needed!(global_state, document, char_position)
39
39
 
40
40
  node_context = RubyDocument.locate(
@@ -24,7 +24,7 @@ module RubyLsp
24
24
 
25
25
  sig { override.returns(T.nilable(Interface::Range)) }
26
26
  def perform
27
- char_position = @document.create_scanner.find_char_position(@position)
27
+ char_position, _ = @document.find_index_by_position(@position)
28
28
 
29
29
  node_context = RubyDocument.locate(
30
30
  @document.parse_result.value,
@@ -30,7 +30,7 @@ module RubyLsp
30
30
  sig { override.returns(T::Array[Interface::Location]) }
31
31
  def perform
32
32
  position = @params[:position]
33
- char_position = @document.create_scanner.find_char_position(position)
33
+ char_position, _ = @document.find_index_by_position(position)
34
34
 
35
35
  node_context = RubyDocument.locate(
36
36
  @document.parse_result.value,
@@ -40,7 +40,7 @@ module RubyLsp
40
40
 
41
41
  sig { override.returns(T.nilable(Interface::WorkspaceEdit)) }
42
42
  def perform
43
- char_position = @document.create_scanner.find_char_position(@position)
43
+ char_position, _ = @document.find_index_by_position(@position)
44
44
 
45
45
  node_context = RubyDocument.locate(
46
46
  @document.parse_result.value,
@@ -116,8 +116,8 @@ module RubyLsp
116
116
  T.must(@global_state.index[fully_qualified_name]).each do |entry|
117
117
  # Do not rename files that are not part of the workspace
118
118
  uri = entry.uri
119
- file_path = T.must(uri.full_path)
120
- next unless file_path.start_with?(@global_state.workspace_path)
119
+ file_path = uri.full_path
120
+ next unless file_path&.start_with?(@global_state.workspace_path)
121
121
 
122
122
  case entry
123
123
  when RubyIndexer::Entry::Class, RubyIndexer::Entry::Module, RubyIndexer::Entry::Constant,
@@ -31,10 +31,7 @@ module RubyLsp
31
31
  sig { returns(String) }
32
32
  def ast_for_range
33
33
  range = T.must(@range)
34
-
35
- scanner = @document.create_scanner
36
- start_char = scanner.find_char_position(range[:start])
37
- end_char = scanner.find_char_position(range[:end])
34
+ start_char, end_char = @document.find_index_by_position(range[:start], range[:end])
38
35
 
39
36
  queue = @tree.statements.body.dup
40
37
  found_nodes = []
@@ -36,7 +36,7 @@ module RubyLsp
36
36
  def initialize(document, global_state, position, context, dispatcher, sorbet_level) # rubocop:disable Metrics/ParameterLists
37
37
  super()
38
38
 
39
- char_position = document.create_scanner.find_char_position(position)
39
+ char_position, _ = document.find_index_by_position(position)
40
40
  delegate_request_if_needed!(global_state, document, char_position)
41
41
 
42
42
  node_context = RubyDocument.locate(
@@ -31,7 +31,7 @@ module RubyLsp
31
31
 
32
32
  # TODO: avoid passing document once we have alternative ways to get at
33
33
  # encoding and file source
34
- sig { params(document: RubyDocument, offense: RuboCop::Cop::Offense, uri: URI::Generic).void }
34
+ sig { params(document: RubyDocument, offense: ::RuboCop::Cop::Offense, uri: URI::Generic).void }
35
35
  def initialize(document, offense, uri)
36
36
  @document = document
37
37
  @offense = offense
@@ -48,7 +48,7 @@ module RubyLsp
48
48
  code_actions
49
49
  end
50
50
 
51
- sig { params(config: RuboCop::Config).returns(Interface::Diagnostic) }
51
+ sig { params(config: ::RuboCop::Config).returns(Interface::Diagnostic) }
52
52
  def to_lsp_diagnostic(config)
53
53
  # highlighted_area contains the begin and end position of the first line
54
54
  # This ensures that multiline offenses don't clutter the editor
@@ -90,7 +90,7 @@ module RubyLsp
90
90
  RUBOCOP_TO_LSP_SEVERITY[@offense.severity.name]
91
91
  end
92
92
 
93
- sig { params(config: RuboCop::Config).returns(T.nilable(Interface::CodeDescription)) }
93
+ sig { params(config: ::RuboCop::Config).returns(T.nilable(Interface::CodeDescription)) }
94
94
  def code_description(config)
95
95
  cop = RuboCopRunner.find_cop_by_name(@offense.cop_name)
96
96
  return unless cop
@@ -40,10 +40,10 @@ module RubyLsp
40
40
  For more details, run RuboCop on the command line.
41
41
  EOS
42
42
 
43
- sig { params(rubocop_error: T.any(RuboCop::ErrorWithAnalyzedFileLocation, StandardError)).void }
43
+ sig { params(rubocop_error: T.any(::RuboCop::ErrorWithAnalyzedFileLocation, StandardError)).void }
44
44
  def initialize(rubocop_error)
45
45
  message = case rubocop_error
46
- when RuboCop::ErrorWithAnalyzedFileLocation
46
+ when ::RuboCop::ErrorWithAnalyzedFileLocation
47
47
  format(MESSAGE, "for the #{rubocop_error.cop.name} cop")
48
48
  when StandardError
49
49
  format(MESSAGE, rubocop_error.message)
@@ -53,7 +53,7 @@ module RubyLsp
53
53
  end
54
54
 
55
55
  # :nodoc:
56
- class RuboCopRunner < RuboCop::Runner
56
+ class RuboCopRunner < ::RuboCop::Runner
57
57
  extend T::Sig
58
58
 
59
59
  class ConfigurationError < StandardError; end
@@ -68,14 +68,14 @@ module RubyLsp
68
68
  T::Array[String],
69
69
  )
70
70
 
71
- sig { returns(T::Array[RuboCop::Cop::Offense]) }
71
+ sig { returns(T::Array[::RuboCop::Cop::Offense]) }
72
72
  attr_reader :offenses
73
73
 
74
74
  sig { returns(::RuboCop::Config) }
75
75
  attr_reader :config_for_working_directory
76
76
 
77
77
  begin
78
- RuboCop::Options.new.parse(["--raise-cop-error"])
78
+ ::RuboCop::Options.new.parse(["--raise-cop-error"])
79
79
  DEFAULT_ARGS << "--raise-cop-error"
80
80
  rescue OptionParser::InvalidOption
81
81
  # older versions of RuboCop don't support this flag
@@ -85,7 +85,7 @@ module RubyLsp
85
85
  sig { params(args: String).void }
86
86
  def initialize(*args)
87
87
  @options = T.let({}, T::Hash[Symbol, T.untyped])
88
- @offenses = T.let([], T::Array[RuboCop::Cop::Offense])
88
+ @offenses = T.let([], T::Array[::RuboCop::Cop::Offense])
89
89
  @errors = T.let([], T::Array[String])
90
90
  @warnings = T.let([], T::Array[String])
91
91
 
@@ -113,9 +113,9 @@ module RubyLsp
113
113
  # RuboCop rescues interrupts and then sets the `@aborting` variable to true. We don't want them to be rescued,
114
114
  # so here we re-raise in case RuboCop received an interrupt.
115
115
  raise Interrupt if aborting?
116
- rescue RuboCop::Runner::InfiniteCorrectionLoop => error
116
+ rescue ::RuboCop::Runner::InfiniteCorrectionLoop => error
117
117
  raise Formatting::Error, error.message
118
- rescue RuboCop::ValidationError => error
118
+ rescue ::RuboCop::ValidationError => error
119
119
  raise ConfigurationError, error.message
120
120
  rescue StandardError => error
121
121
  raise InternalRuboCopError, error
@@ -129,25 +129,25 @@ module RubyLsp
129
129
  class << self
130
130
  extend T::Sig
131
131
 
132
- sig { params(cop_name: String).returns(T.nilable(T.class_of(RuboCop::Cop::Base))) }
132
+ sig { params(cop_name: String).returns(T.nilable(T.class_of(::RuboCop::Cop::Base))) }
133
133
  def find_cop_by_name(cop_name)
134
134
  cop_registry[cop_name]&.first
135
135
  end
136
136
 
137
137
  private
138
138
 
139
- sig { returns(T::Hash[String, [T.class_of(RuboCop::Cop::Base)]]) }
139
+ sig { returns(T::Hash[String, [T.class_of(::RuboCop::Cop::Base)]]) }
140
140
  def cop_registry
141
141
  @cop_registry ||= T.let(
142
- RuboCop::Cop::Registry.global.to_h,
143
- T.nilable(T::Hash[String, [T.class_of(RuboCop::Cop::Base)]]),
142
+ ::RuboCop::Cop::Registry.global.to_h,
143
+ T.nilable(T::Hash[String, [T.class_of(::RuboCop::Cop::Base)]]),
144
144
  )
145
145
  end
146
146
  end
147
147
 
148
148
  private
149
149
 
150
- sig { params(_file: String, offenses: T::Array[RuboCop::Cop::Offense]).void }
150
+ sig { params(_file: String, offenses: T::Array[::RuboCop::Cop::Offense]).void }
151
151
  def file_finished(_file, offenses)
152
152
  @offenses = offenses
153
153
  end
@@ -22,10 +22,10 @@ module RubyLsp
22
22
  def perform
23
23
  @index.fuzzy_search(@query).filter_map do |entry|
24
24
  uri = entry.uri
25
- file_path = T.must(uri.full_path)
25
+ file_path = uri.full_path
26
26
 
27
27
  # We only show symbols declared in the workspace
28
- in_dependencies = !not_in_dependencies?(file_path)
28
+ in_dependencies = file_path && !not_in_dependencies?(file_path)
29
29
  next if in_dependencies
30
30
 
31
31
  # We should never show private symbols when searching the entire workspace
@@ -8,6 +8,22 @@ module RubyLsp
8
8
 
9
9
  ParseResultType = type_member { { fixed: Prism::ParseResult } }
10
10
 
11
+ METHODS_THAT_CHANGE_DECLARATIONS = [
12
+ :private_constant,
13
+ :attr_reader,
14
+ :attr_writer,
15
+ :attr_accessor,
16
+ :alias_method,
17
+ :include,
18
+ :prepend,
19
+ :extend,
20
+ :public,
21
+ :protected,
22
+ :private,
23
+ :module_function,
24
+ :private_class_method,
25
+ ].freeze
26
+
11
27
  class SorbetLevel < T::Enum
12
28
  enums do
13
29
  None = new("none")
@@ -142,8 +158,8 @@ module RubyLsp
142
158
  end
143
159
  attr_reader :code_units_cache
144
160
 
145
- sig { params(source: String, version: Integer, uri: URI::Generic, encoding: Encoding).void }
146
- def initialize(source:, version:, uri:, encoding: Encoding::UTF_8)
161
+ sig { params(source: String, version: Integer, uri: URI::Generic, global_state: GlobalState).void }
162
+ def initialize(source:, version:, uri:, global_state:)
147
163
  super
148
164
  @code_units_cache = T.let(@parse_result.code_units_cache(@encoding), T.any(
149
165
  T.proc.params(arg0: Integer).returns(Integer),
@@ -198,9 +214,8 @@ module RubyLsp
198
214
  ).returns(T.nilable(Prism::Node))
199
215
  end
200
216
  def locate_first_within_range(range, node_types: [])
201
- scanner = create_scanner
202
- start_position = scanner.find_char_position(range[:start])
203
- end_position = scanner.find_char_position(range[:end])
217
+ start_position, end_position = find_index_by_position(range[:start], range[:end])
218
+
204
219
  desired_range = (start_position...end_position)
205
220
  queue = T.let(@parse_result.value.child_nodes.compact, T::Array[T.nilable(Prism::Node)])
206
221
 
@@ -232,12 +247,66 @@ module RubyLsp
232
247
  ).returns(NodeContext)
233
248
  end
234
249
  def locate_node(position, node_types: [])
250
+ char_position, _ = find_index_by_position(position)
251
+
235
252
  RubyDocument.locate(
236
253
  @parse_result.value,
237
- create_scanner.find_char_position(position),
254
+ char_position,
238
255
  code_units_cache: @code_units_cache,
239
256
  node_types: node_types,
240
257
  )
241
258
  end
259
+
260
+ sig { returns(T::Boolean) }
261
+ def last_edit_may_change_declarations?
262
+ # This method controls when we should index documents. If there's no recent edit and the document has just been
263
+ # opened, we need to index it
264
+ return true unless @last_edit
265
+
266
+ case @last_edit
267
+ when Delete
268
+ # Not optimized yet. It's not trivial to identify that a declaration has been removed since the source is no
269
+ # longer there and we don't remember the deleted text
270
+ true
271
+ when Insert, Replace
272
+ position_may_impact_declarations?(@last_edit.range[:start])
273
+ else
274
+ false
275
+ end
276
+ end
277
+
278
+ private
279
+
280
+ sig { params(position: T::Hash[Symbol, Integer]).returns(T::Boolean) }
281
+ def position_may_impact_declarations?(position)
282
+ node_context = locate_node(position)
283
+ node_at_edit = node_context.node
284
+
285
+ # Adjust to the parent when editing the constant of a class/module declaration
286
+ if node_at_edit.is_a?(Prism::ConstantReadNode) || node_at_edit.is_a?(Prism::ConstantPathNode)
287
+ node_at_edit = node_context.parent
288
+ end
289
+
290
+ case node_at_edit
291
+ when Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode, Prism::DefNode,
292
+ Prism::ConstantPathWriteNode, Prism::ConstantPathOrWriteNode, Prism::ConstantPathOperatorWriteNode,
293
+ Prism::ConstantPathAndWriteNode, Prism::ConstantOrWriteNode, Prism::ConstantWriteNode,
294
+ Prism::ConstantAndWriteNode, Prism::ConstantOperatorWriteNode, Prism::GlobalVariableAndWriteNode,
295
+ Prism::GlobalVariableOperatorWriteNode, Prism::GlobalVariableOrWriteNode, Prism::GlobalVariableTargetNode,
296
+ Prism::GlobalVariableWriteNode, Prism::InstanceVariableWriteNode, Prism::InstanceVariableAndWriteNode,
297
+ Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableOrWriteNode,
298
+ Prism::InstanceVariableTargetNode, Prism::AliasMethodNode
299
+ true
300
+ when Prism::MultiWriteNode
301
+ [*node_at_edit.lefts, *node_at_edit.rest, *node_at_edit.rights].any? do |node|
302
+ node.is_a?(Prism::ConstantTargetNode) || node.is_a?(Prism::ConstantPathTargetNode)
303
+ end
304
+ when Prism::CallNode
305
+ receiver = node_at_edit.receiver
306
+ (!receiver || receiver.is_a?(Prism::SelfNode)) && METHODS_THAT_CHANGE_DECLARATIONS.include?(node_at_edit.name)
307
+ else
308
+ false
309
+ end
310
+ end
242
311
  end
243
312
  end