ruby-lsp 0.23.1 → 0.23.5

Sign up to get free protection for your applications and to get access to all the features.
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