ruby-lsp 0.14.6 → 0.16.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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/VERSION +1 -1
- data/exe/ruby-lsp +1 -16
- data/exe/ruby-lsp-check +13 -22
- data/exe/ruby-lsp-doctor +9 -0
- data/lib/ruby_indexer/lib/ruby_indexer/collector.rb +14 -1
- data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +11 -23
- data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +32 -8
- data/lib/ruby_indexer/lib/ruby_indexer/location.rb +26 -0
- data/lib/ruby_indexer/ruby_indexer.rb +1 -0
- data/lib/ruby_indexer/test/classes_and_modules_test.rb +46 -0
- data/lib/ruby_indexer/test/configuration_test.rb +2 -11
- data/lib/ruby_lsp/addon.rb +18 -9
- data/lib/ruby_lsp/base_server.rb +149 -0
- data/lib/ruby_lsp/document.rb +6 -11
- data/lib/ruby_lsp/global_state.rb +180 -0
- data/lib/ruby_lsp/internal.rb +4 -1
- data/lib/ruby_lsp/listeners/code_lens.rb +22 -13
- data/lib/ruby_lsp/listeners/completion.rb +13 -14
- data/lib/ruby_lsp/listeners/definition.rb +14 -6
- data/lib/ruby_lsp/listeners/document_symbol.rb +91 -3
- data/lib/ruby_lsp/listeners/hover.rb +6 -5
- data/lib/ruby_lsp/listeners/signature_help.rb +7 -4
- data/lib/ruby_lsp/load_sorbet.rb +62 -0
- data/lib/ruby_lsp/requests/code_lens.rb +3 -2
- data/lib/ruby_lsp/requests/completion.rb +15 -4
- data/lib/ruby_lsp/requests/completion_resolve.rb +56 -0
- data/lib/ruby_lsp/requests/definition.rb +11 -4
- data/lib/ruby_lsp/requests/diagnostics.rb +8 -11
- data/lib/ruby_lsp/requests/document_symbol.rb +3 -3
- data/lib/ruby_lsp/requests/formatting.rb +7 -43
- data/lib/ruby_lsp/requests/hover.rb +4 -4
- data/lib/ruby_lsp/requests/request.rb +2 -0
- data/lib/ruby_lsp/requests/semantic_highlighting.rb +7 -4
- data/lib/ruby_lsp/requests/signature_help.rb +4 -3
- data/lib/ruby_lsp/requests/support/common.rb +16 -5
- data/lib/ruby_lsp/requests/support/formatter.rb +26 -0
- data/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb +1 -1
- data/lib/ruby_lsp/requests/support/rubocop_formatter.rb +47 -0
- data/lib/ruby_lsp/requests/support/rubocop_runner.rb +4 -0
- data/lib/ruby_lsp/requests/support/{syntax_tree_formatting_runner.rb → syntax_tree_formatter.rb} +13 -6
- data/lib/ruby_lsp/requests/workspace_symbol.rb +5 -4
- data/lib/ruby_lsp/requests.rb +3 -1
- data/lib/ruby_lsp/response_builders/semantic_highlighting.rb +36 -13
- data/lib/ruby_lsp/server.rb +763 -142
- data/lib/ruby_lsp/setup_bundler.rb +13 -1
- data/lib/ruby_lsp/store.rb +3 -15
- data/lib/ruby_lsp/test_helper.rb +52 -0
- data/lib/ruby_lsp/utils.rb +68 -33
- metadata +16 -13
- data/lib/ruby_lsp/executor.rb +0 -614
- data/lib/ruby_lsp/requests/support/dependency_detector.rb +0 -93
- data/lib/ruby_lsp/requests/support/formatter_runner.rb +0 -18
- data/lib/ruby_lsp/requests/support/rubocop_diagnostics_runner.rb +0 -34
- data/lib/ruby_lsp/requests/support/rubocop_formatting_runner.rb +0 -35
@@ -0,0 +1,149 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module RubyLsp
|
5
|
+
class BaseServer
|
6
|
+
extend T::Sig
|
7
|
+
extend T::Helpers
|
8
|
+
|
9
|
+
abstract!
|
10
|
+
|
11
|
+
sig { params(test_mode: T::Boolean).void }
|
12
|
+
def initialize(test_mode: false)
|
13
|
+
@test_mode = T.let(test_mode, T::Boolean)
|
14
|
+
@writer = T.let(Transport::Stdio::Writer.new, Transport::Stdio::Writer)
|
15
|
+
@reader = T.let(Transport::Stdio::Reader.new, Transport::Stdio::Reader)
|
16
|
+
@incoming_queue = T.let(Thread::Queue.new, Thread::Queue)
|
17
|
+
@outgoing_queue = T.let(Thread::Queue.new, Thread::Queue)
|
18
|
+
@cancelled_requests = T.let([], T::Array[Integer])
|
19
|
+
@mutex = T.let(Mutex.new, Mutex)
|
20
|
+
@worker = T.let(new_worker, Thread)
|
21
|
+
@current_request_id = T.let(1, Integer)
|
22
|
+
@store = T.let(Store.new, Store)
|
23
|
+
@outgoing_dispatcher = T.let(
|
24
|
+
Thread.new do
|
25
|
+
unless test_mode
|
26
|
+
while (message = @outgoing_queue.pop)
|
27
|
+
@mutex.synchronize { @writer.write(message.to_hash) }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end,
|
31
|
+
Thread,
|
32
|
+
)
|
33
|
+
|
34
|
+
Thread.main.priority = 1
|
35
|
+
end
|
36
|
+
|
37
|
+
sig { void }
|
38
|
+
def start
|
39
|
+
@reader.read do |message|
|
40
|
+
method = message[:method]
|
41
|
+
|
42
|
+
# We must parse the document under a mutex lock or else we might switch threads and accept text edits in the
|
43
|
+
# source. Altering the source reference during parsing will put the parser in an invalid internal state, since
|
44
|
+
# it started parsing with one source but then it changed in the middle. We don't want to do this for text
|
45
|
+
# synchronization notifications
|
46
|
+
@mutex.synchronize do
|
47
|
+
uri = message.dig(:params, :textDocument, :uri)
|
48
|
+
|
49
|
+
if uri
|
50
|
+
begin
|
51
|
+
parsed_uri = URI(uri)
|
52
|
+
message[:params][:textDocument][:uri] = parsed_uri
|
53
|
+
|
54
|
+
# We don't want to try to parse documents on text synchronization notifications
|
55
|
+
@store.get(parsed_uri).parse unless method.start_with?("textDocument/did")
|
56
|
+
rescue Errno::ENOENT
|
57
|
+
# If we receive a request for a file that no longer exists, we don't want to fail
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# The following requests need to be executed in the main thread directly to avoid concurrency issues. Everything
|
63
|
+
# else is pushed into the incoming queue
|
64
|
+
case method
|
65
|
+
when "initialize", "initialized", "textDocument/didOpen", "textDocument/didClose", "textDocument/didChange"
|
66
|
+
process_message(message)
|
67
|
+
when "shutdown"
|
68
|
+
$stderr.puts("Shutting down Ruby LSP...")
|
69
|
+
|
70
|
+
shutdown
|
71
|
+
|
72
|
+
@mutex.synchronize do
|
73
|
+
run_shutdown
|
74
|
+
@writer.write(Result.new(id: message[:id], response: nil).to_hash)
|
75
|
+
end
|
76
|
+
when "exit"
|
77
|
+
@mutex.synchronize do
|
78
|
+
status = @incoming_queue.closed? ? 0 : 1
|
79
|
+
$stderr.puts("Shutdown complete with status #{status}")
|
80
|
+
exit(status)
|
81
|
+
end
|
82
|
+
else
|
83
|
+
@incoming_queue << message
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
sig { void }
|
89
|
+
def run_shutdown
|
90
|
+
@incoming_queue.clear
|
91
|
+
@outgoing_queue.clear
|
92
|
+
@incoming_queue.close
|
93
|
+
@outgoing_queue.close
|
94
|
+
@cancelled_requests.clear
|
95
|
+
|
96
|
+
@worker.join
|
97
|
+
@outgoing_dispatcher.join
|
98
|
+
@store.clear
|
99
|
+
end
|
100
|
+
|
101
|
+
# This method is only intended to be used in tests! Pops the latest response that would be sent to the client
|
102
|
+
sig { returns(T.untyped) }
|
103
|
+
def pop_response
|
104
|
+
@outgoing_queue.pop
|
105
|
+
end
|
106
|
+
|
107
|
+
sig { abstract.params(message: T::Hash[Symbol, T.untyped]).void }
|
108
|
+
def process_message(message); end
|
109
|
+
|
110
|
+
sig { abstract.void }
|
111
|
+
def shutdown; end
|
112
|
+
|
113
|
+
sig { returns(Thread) }
|
114
|
+
def new_worker
|
115
|
+
Thread.new do
|
116
|
+
while (message = T.let(@incoming_queue.pop, T.nilable(T::Hash[Symbol, T.untyped])))
|
117
|
+
id = message[:id]
|
118
|
+
|
119
|
+
# Check if the request was cancelled before trying to process it
|
120
|
+
@mutex.synchronize do
|
121
|
+
if id && @cancelled_requests.include?(id)
|
122
|
+
send_message(Result.new(id: id, response: nil))
|
123
|
+
@cancelled_requests.delete(id)
|
124
|
+
next
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
process_message(message)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
sig { params(message: T.any(Result, Error, Notification, Request)).void }
|
134
|
+
def send_message(message)
|
135
|
+
# When we're shutting down the server, there's a small race condition between closing the thread queues and
|
136
|
+
# finishing remaining requests. We may close the queue in the middle of processing a request, which will then fail
|
137
|
+
# when trying to send a response back
|
138
|
+
return if @outgoing_queue.closed?
|
139
|
+
|
140
|
+
@outgoing_queue << message
|
141
|
+
@current_request_id += 1 if message.is_a?(Request)
|
142
|
+
end
|
143
|
+
|
144
|
+
sig { params(id: Integer).void }
|
145
|
+
def send_empty_response(id)
|
146
|
+
send_message(Result.new(id: id, response: nil))
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
data/lib/ruby_lsp/document.rb
CHANGED
@@ -20,13 +20,13 @@ module RubyLsp
|
|
20
20
|
sig { returns(URI::Generic) }
|
21
21
|
attr_reader :uri
|
22
22
|
|
23
|
-
sig { returns(
|
23
|
+
sig { returns(Encoding) }
|
24
24
|
attr_reader :encoding
|
25
25
|
|
26
|
-
sig { params(source: String, version: Integer, uri: URI::Generic, encoding:
|
27
|
-
def initialize(source:, version:, uri:, encoding:
|
26
|
+
sig { params(source: String, version: Integer, uri: URI::Generic, encoding: Encoding).void }
|
27
|
+
def initialize(source:, version:, uri:, encoding: Encoding::UTF_8)
|
28
28
|
@cache = T.let({}, T::Hash[String, T.untyped])
|
29
|
-
@encoding = T.let(encoding,
|
29
|
+
@encoding = T.let(encoding, Encoding)
|
30
30
|
@source = T.let(source, String)
|
31
31
|
@version = T.let(version, Integer)
|
32
32
|
@uri = T.let(uri, URI::Generic)
|
@@ -180,11 +180,6 @@ module RubyLsp
|
|
180
180
|
end
|
181
181
|
end
|
182
182
|
|
183
|
-
sig { returns(T::Boolean) }
|
184
|
-
def typechecker_enabled?
|
185
|
-
DependencyDetector.instance.typechecker && sorbet_sigil_is_true_or_higher
|
186
|
-
end
|
187
|
-
|
188
183
|
class Scanner
|
189
184
|
extend T::Sig
|
190
185
|
|
@@ -192,7 +187,7 @@ module RubyLsp
|
|
192
187
|
# After character 0xFFFF, UTF-16 considers characters to have length 2 and we have to account for that
|
193
188
|
SURROGATE_PAIR_START = T.let(0xFFFF, Integer)
|
194
189
|
|
195
|
-
sig { params(source: String, encoding:
|
190
|
+
sig { params(source: String, encoding: Encoding).void }
|
196
191
|
def initialize(source, encoding)
|
197
192
|
@current_line = T.let(0, Integer)
|
198
193
|
@pos = T.let(0, Integer)
|
@@ -214,7 +209,7 @@ module RubyLsp
|
|
214
209
|
# need to adjust for surrogate pairs
|
215
210
|
requested_position = @pos + position[:character]
|
216
211
|
|
217
|
-
if @encoding ==
|
212
|
+
if @encoding == Encoding::UTF_16LE
|
218
213
|
requested_position -= utf_16_character_position_correction(@pos, requested_position)
|
219
214
|
end
|
220
215
|
|
@@ -0,0 +1,180 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module RubyLsp
|
5
|
+
class GlobalState
|
6
|
+
extend T::Sig
|
7
|
+
|
8
|
+
sig { returns(String) }
|
9
|
+
attr_reader :test_library
|
10
|
+
|
11
|
+
sig { returns(String) }
|
12
|
+
attr_accessor :formatter
|
13
|
+
|
14
|
+
sig { returns(T::Boolean) }
|
15
|
+
attr_reader :typechecker
|
16
|
+
|
17
|
+
sig { returns(RubyIndexer::Index) }
|
18
|
+
attr_reader :index
|
19
|
+
|
20
|
+
sig { returns(Encoding) }
|
21
|
+
attr_reader :encoding
|
22
|
+
|
23
|
+
sig { returns(T::Boolean) }
|
24
|
+
attr_reader :supports_watching_files
|
25
|
+
|
26
|
+
sig { void }
|
27
|
+
def initialize
|
28
|
+
@workspace_uri = T.let(URI::Generic.from_path(path: Dir.pwd), URI::Generic)
|
29
|
+
@encoding = T.let(Encoding::UTF_8, Encoding)
|
30
|
+
|
31
|
+
@formatter = T.let("auto", String)
|
32
|
+
@linters = T.let([], T::Array[String])
|
33
|
+
@test_library = T.let("minitest", String)
|
34
|
+
@typechecker = T.let(true, T::Boolean)
|
35
|
+
@index = T.let(RubyIndexer::Index.new, RubyIndexer::Index)
|
36
|
+
@supported_formatters = T.let({}, T::Hash[String, Requests::Support::Formatter])
|
37
|
+
@supports_watching_files = T.let(false, T::Boolean)
|
38
|
+
end
|
39
|
+
|
40
|
+
sig { params(identifier: String, instance: Requests::Support::Formatter).void }
|
41
|
+
def register_formatter(identifier, instance)
|
42
|
+
@supported_formatters[identifier] = instance
|
43
|
+
end
|
44
|
+
|
45
|
+
sig { returns(T.nilable(Requests::Support::Formatter)) }
|
46
|
+
def active_formatter
|
47
|
+
@supported_formatters[@formatter]
|
48
|
+
end
|
49
|
+
|
50
|
+
sig { returns(T::Array[Requests::Support::Formatter]) }
|
51
|
+
def active_linters
|
52
|
+
@linters.filter_map { |name| @supported_formatters[name] }
|
53
|
+
end
|
54
|
+
|
55
|
+
sig { params(options: T::Hash[Symbol, T.untyped]).void }
|
56
|
+
def apply_options(options)
|
57
|
+
dependencies = gather_dependencies
|
58
|
+
workspace_uri = options.dig(:workspaceFolders, 0, :uri)
|
59
|
+
@workspace_uri = URI(workspace_uri) if workspace_uri
|
60
|
+
|
61
|
+
specified_formatter = options.dig(:initializationOptions, :formatter)
|
62
|
+
@formatter = specified_formatter if specified_formatter
|
63
|
+
@formatter = detect_formatter(dependencies) if @formatter == "auto"
|
64
|
+
|
65
|
+
specified_linters = options.dig(:initializationOptions, :linters)
|
66
|
+
@linters = specified_linters || detect_linters(dependencies)
|
67
|
+
@test_library = detect_test_library(dependencies)
|
68
|
+
@typechecker = detect_typechecker(dependencies)
|
69
|
+
|
70
|
+
encodings = options.dig(:capabilities, :general, :positionEncodings)
|
71
|
+
@encoding = if !encodings || encodings.empty?
|
72
|
+
Encoding::UTF_16LE
|
73
|
+
elsif encodings.include?(Constant::PositionEncodingKind::UTF8)
|
74
|
+
Encoding::UTF_8
|
75
|
+
elsif encodings.include?(Constant::PositionEncodingKind::UTF16)
|
76
|
+
Encoding::UTF_16LE
|
77
|
+
else
|
78
|
+
Encoding::UTF_32
|
79
|
+
end
|
80
|
+
|
81
|
+
file_watching_caps = options.dig(:capabilities, :workspace, :didChangeWatchedFiles)
|
82
|
+
if file_watching_caps&.dig(:dynamicRegistration) && file_watching_caps&.dig(:relativePatternSupport)
|
83
|
+
@supports_watching_files = true
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
sig { returns(String) }
|
88
|
+
def workspace_path
|
89
|
+
T.must(@workspace_uri.to_standardized_path)
|
90
|
+
end
|
91
|
+
|
92
|
+
sig { returns(String) }
|
93
|
+
def encoding_name
|
94
|
+
case @encoding
|
95
|
+
when Encoding::UTF_8
|
96
|
+
Constant::PositionEncodingKind::UTF8
|
97
|
+
when Encoding::UTF_16LE
|
98
|
+
Constant::PositionEncodingKind::UTF16
|
99
|
+
else
|
100
|
+
Constant::PositionEncodingKind::UTF32
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
sig { params(dependencies: T::Array[String]).returns(String) }
|
107
|
+
def detect_formatter(dependencies)
|
108
|
+
# NOTE: Intentionally no $ at end, since we want to match rubocop-shopify, etc.
|
109
|
+
if dependencies.any?(/^rubocop/)
|
110
|
+
"rubocop"
|
111
|
+
elsif dependencies.any?(/^syntax_tree$/)
|
112
|
+
"syntax_tree"
|
113
|
+
else
|
114
|
+
"none"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Try to detect if there are linters in the project's dependencies. For auto-detection, we always only consider a
|
119
|
+
# single linter. To have multiple linters running, the user must configure them manually
|
120
|
+
sig { params(dependencies: T::Array[String]).returns(T::Array[String]) }
|
121
|
+
def detect_linters(dependencies)
|
122
|
+
linters = []
|
123
|
+
linters << "rubocop" if dependencies.any?(/^rubocop/)
|
124
|
+
linters
|
125
|
+
end
|
126
|
+
|
127
|
+
sig { params(dependencies: T::Array[String]).returns(String) }
|
128
|
+
def detect_test_library(dependencies)
|
129
|
+
if dependencies.any?(/^rspec/)
|
130
|
+
"rspec"
|
131
|
+
# A Rails app may have a dependency on minitest, but we would instead want to use the Rails test runner provided
|
132
|
+
# by ruby-lsp-rails. A Rails app doesn't need to depend on the rails gem itself, individual components like
|
133
|
+
# activestorage may be added to the gemfile so that other components aren't downloaded. Check for the presence
|
134
|
+
# of bin/rails to support these cases.
|
135
|
+
elsif File.exist?(File.join(workspace_path, "bin/rails"))
|
136
|
+
"rails"
|
137
|
+
# NOTE: Intentionally ends with $ to avoid mis-matching minitest-reporters, etc. in a Rails app.
|
138
|
+
elsif dependencies.any?(/^minitest$/)
|
139
|
+
"minitest"
|
140
|
+
elsif dependencies.any?(/^test-unit/)
|
141
|
+
"test-unit"
|
142
|
+
else
|
143
|
+
"unknown"
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
sig { params(dependencies: T::Array[String]).returns(T::Boolean) }
|
148
|
+
def detect_typechecker(dependencies)
|
149
|
+
return false if ENV["RUBY_LSP_BYPASS_TYPECHECKER"]
|
150
|
+
|
151
|
+
# We can't read the env from within `Bundle.with_original_env` so we need to set it here.
|
152
|
+
ruby_lsp_env_is_test = (ENV["RUBY_LSP_ENV"] == "test")
|
153
|
+
Bundler.with_original_env do
|
154
|
+
sorbet_static_detected = dependencies.any?(/^sorbet-static/)
|
155
|
+
# Don't show message while running tests, since it's noisy
|
156
|
+
if sorbet_static_detected && !ruby_lsp_env_is_test
|
157
|
+
$stderr.puts("Ruby LSP detected this is a Sorbet project so will defer to Sorbet LSP for some functionality")
|
158
|
+
end
|
159
|
+
sorbet_static_detected
|
160
|
+
end
|
161
|
+
rescue Bundler::GemfileNotFound
|
162
|
+
false
|
163
|
+
end
|
164
|
+
|
165
|
+
sig { returns(T::Array[String]) }
|
166
|
+
def gather_dependencies
|
167
|
+
Bundler.with_original_env { Bundler.default_gemfile }
|
168
|
+
Bundler.locked_gems.dependencies.keys + gemspec_dependencies
|
169
|
+
rescue Bundler::GemfileNotFound
|
170
|
+
[]
|
171
|
+
end
|
172
|
+
|
173
|
+
sig { returns(T::Array[String]) }
|
174
|
+
def gemspec_dependencies
|
175
|
+
Bundler.locked_gems.sources
|
176
|
+
.grep(Bundler::Source::Gemspec)
|
177
|
+
.flat_map { _1.gemspec&.dependencies&.map(&:name) }
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
data/lib/ruby_lsp/internal.rb
CHANGED
@@ -20,12 +20,13 @@ require "prism/visitor"
|
|
20
20
|
require "language_server-protocol"
|
21
21
|
|
22
22
|
require "ruby-lsp"
|
23
|
+
require "ruby_lsp/base_server"
|
23
24
|
require "ruby_indexer/ruby_indexer"
|
24
25
|
require "core_ext/uri"
|
25
26
|
require "ruby_lsp/utils"
|
26
27
|
require "ruby_lsp/parameter_scope"
|
28
|
+
require "ruby_lsp/global_state"
|
27
29
|
require "ruby_lsp/server"
|
28
|
-
require "ruby_lsp/executor"
|
29
30
|
require "ruby_lsp/requests"
|
30
31
|
require "ruby_lsp/response_builders"
|
31
32
|
require "ruby_lsp/document"
|
@@ -33,3 +34,5 @@ require "ruby_lsp/ruby_document"
|
|
33
34
|
require "ruby_lsp/store"
|
34
35
|
require "ruby_lsp/addon"
|
35
36
|
require "ruby_lsp/requests/support/rubocop_runner"
|
37
|
+
require "ruby_lsp/requests/support/rubocop_formatter"
|
38
|
+
require "ruby_lsp/requests/support/syntax_tree_formatter"
|
@@ -27,12 +27,14 @@ module RubyLsp
|
|
27
27
|
sig do
|
28
28
|
params(
|
29
29
|
response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::CodeLens],
|
30
|
+
global_state: GlobalState,
|
30
31
|
uri: URI::Generic,
|
31
32
|
dispatcher: Prism::Dispatcher,
|
32
33
|
).void
|
33
34
|
end
|
34
|
-
def initialize(response_builder, uri, dispatcher)
|
35
|
+
def initialize(response_builder, global_state, uri, dispatcher)
|
35
36
|
@response_builder = response_builder
|
37
|
+
@global_state = global_state
|
36
38
|
@uri = T.let(uri, URI::Generic)
|
37
39
|
@path = T.let(uri.to_standardized_path, T.nilable(String))
|
38
40
|
# visibility_stack is a stack of [current_visibility, previous_visibility]
|
@@ -66,17 +68,22 @@ module RubyLsp
|
|
66
68
|
command: generate_test_command(group_stack: @group_stack),
|
67
69
|
kind: :group,
|
68
70
|
)
|
69
|
-
end
|
70
71
|
|
71
|
-
|
72
|
-
|
72
|
+
@group_id_stack.push(@group_id)
|
73
|
+
@group_id += 1
|
74
|
+
end
|
73
75
|
end
|
74
76
|
|
75
77
|
sig { params(node: Prism::ClassNode).void }
|
76
78
|
def on_class_node_leave(node)
|
77
79
|
@visibility_stack.pop
|
78
80
|
@group_stack.pop
|
79
|
-
|
81
|
+
|
82
|
+
class_name = node.constant_path.slice
|
83
|
+
|
84
|
+
if @path && class_name.end_with?("Test")
|
85
|
+
@group_id_stack.pop
|
86
|
+
end
|
80
87
|
end
|
81
88
|
|
82
89
|
sig { params(node: Prism::DefNode).void }
|
@@ -156,7 +163,7 @@ module RubyLsp
|
|
156
163
|
sig { params(node: Prism::Node, name: String, command: String, kind: Symbol).void }
|
157
164
|
def add_test_code_lens(node, name:, command:, kind:)
|
158
165
|
# don't add code lenses if the test library is not supported or unknown
|
159
|
-
return unless SUPPORTED_TEST_LIBRARIES.include?(
|
166
|
+
return unless SUPPORTED_TEST_LIBRARIES.include?(@global_state.test_library) && @path
|
160
167
|
|
161
168
|
arguments = [
|
162
169
|
@path,
|
@@ -208,7 +215,7 @@ module RubyLsp
|
|
208
215
|
def generate_test_command(group_stack: [], spec_name: nil, method_name: nil)
|
209
216
|
command = BASE_COMMAND + T.must(@path)
|
210
217
|
|
211
|
-
case
|
218
|
+
case @global_state.test_library
|
212
219
|
when "minitest"
|
213
220
|
last_dynamic_reference_index = group_stack.rindex(DYNAMIC_REFERENCE_MARKER)
|
214
221
|
command += if last_dynamic_reference_index
|
@@ -264,12 +271,14 @@ module RubyLsp
|
|
264
271
|
|
265
272
|
return unless name
|
266
273
|
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
274
|
+
if @path
|
275
|
+
add_test_code_lens(
|
276
|
+
node,
|
277
|
+
name: name,
|
278
|
+
command: generate_test_command(spec_name: name),
|
279
|
+
kind: kind,
|
280
|
+
)
|
281
|
+
end
|
273
282
|
end
|
274
283
|
end
|
275
284
|
end
|
@@ -10,16 +10,17 @@ module RubyLsp
|
|
10
10
|
sig do
|
11
11
|
params(
|
12
12
|
response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::CompletionItem],
|
13
|
-
|
13
|
+
global_state: GlobalState,
|
14
14
|
nesting: T::Array[String],
|
15
15
|
typechecker_enabled: T::Boolean,
|
16
16
|
dispatcher: Prism::Dispatcher,
|
17
17
|
uri: URI::Generic,
|
18
18
|
).void
|
19
19
|
end
|
20
|
-
def initialize(response_builder,
|
20
|
+
def initialize(response_builder, global_state, nesting, typechecker_enabled, dispatcher, uri) # rubocop:disable Metrics/ParameterLists
|
21
21
|
@response_builder = response_builder
|
22
|
-
@
|
22
|
+
@global_state = global_state
|
23
|
+
@index = T.let(global_state.index, RubyIndexer::Index)
|
23
24
|
@nesting = nesting
|
24
25
|
@typechecker_enabled = typechecker_enabled
|
25
26
|
@uri = uri
|
@@ -35,7 +36,7 @@ module RubyLsp
|
|
35
36
|
# Handle completion on regular constant references (e.g. `Bar`)
|
36
37
|
sig { params(node: Prism::ConstantReadNode).void }
|
37
38
|
def on_constant_read_node_enter(node)
|
38
|
-
return if
|
39
|
+
return if @global_state.typechecker
|
39
40
|
|
40
41
|
name = constant_name(node)
|
41
42
|
return if name.nil?
|
@@ -56,7 +57,7 @@ module RubyLsp
|
|
56
57
|
# Handle completion on namespaced constant references (e.g. `Foo::Bar`)
|
57
58
|
sig { params(node: Prism::ConstantPathNode).void }
|
58
59
|
def on_constant_path_node_enter(node)
|
59
|
-
return if
|
60
|
+
return if @global_state.typechecker
|
60
61
|
|
61
62
|
name = constant_name(node)
|
62
63
|
return if name.nil?
|
@@ -77,7 +78,10 @@ module RubyLsp
|
|
77
78
|
|
78
79
|
real_namespace = @index.follow_aliased_namespace(T.must(namespace_entries.first).name)
|
79
80
|
|
80
|
-
candidates = @index.prefix_search(
|
81
|
+
candidates = @index.prefix_search(
|
82
|
+
"#{real_namespace}::#{incomplete_name}",
|
83
|
+
top_level_reference ? [] : @nesting,
|
84
|
+
)
|
81
85
|
candidates.each do |entries|
|
82
86
|
# The only time we may have a private constant reference from outside of the namespace is if we're dealing
|
83
87
|
# with ConstantPath and the entry name doesn't start with the current nesting
|
@@ -124,7 +128,9 @@ module RubyLsp
|
|
124
128
|
|
125
129
|
return unless path_node_to_complete.is_a?(Prism::StringNode)
|
126
130
|
|
127
|
-
@index.search_require_paths(path_node_to_complete.content)
|
131
|
+
matched_indexable_paths = @index.search_require_paths(path_node_to_complete.content)
|
132
|
+
|
133
|
+
matched_indexable_paths.map!(&:require_path).sort!.each do |path|
|
128
134
|
@response_builder << build_completion(T.must(path), path_node_to_complete)
|
129
135
|
end
|
130
136
|
end
|
@@ -284,13 +290,6 @@ module RubyLsp
|
|
284
290
|
new_text: insertion_text,
|
285
291
|
),
|
286
292
|
kind: kind,
|
287
|
-
label_details: Interface::CompletionItemLabelDetails.new(
|
288
|
-
description: entries.map(&:file_name).join(","),
|
289
|
-
),
|
290
|
-
documentation: Interface::MarkupContent.new(
|
291
|
-
kind: "markdown",
|
292
|
-
value: markdown_from_index_entries(real_name, entries),
|
293
|
-
),
|
294
293
|
)
|
295
294
|
end
|
296
295
|
|
@@ -7,21 +7,24 @@ module RubyLsp
|
|
7
7
|
extend T::Sig
|
8
8
|
include Requests::Support::Common
|
9
9
|
|
10
|
+
MAX_NUMBER_OF_DEFINITION_CANDIDATES_WITHOUT_RECEIVER = 10
|
11
|
+
|
10
12
|
sig do
|
11
13
|
params(
|
12
14
|
response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::Location],
|
15
|
+
global_state: GlobalState,
|
13
16
|
uri: URI::Generic,
|
14
17
|
nesting: T::Array[String],
|
15
|
-
index: RubyIndexer::Index,
|
16
18
|
dispatcher: Prism::Dispatcher,
|
17
19
|
typechecker_enabled: T::Boolean,
|
18
20
|
).void
|
19
21
|
end
|
20
|
-
def initialize(response_builder, uri, nesting,
|
22
|
+
def initialize(response_builder, global_state, uri, nesting, dispatcher, typechecker_enabled) # rubocop:disable Metrics/ParameterLists
|
21
23
|
@response_builder = response_builder
|
24
|
+
@global_state = global_state
|
25
|
+
@index = T.let(global_state.index, RubyIndexer::Index)
|
22
26
|
@uri = uri
|
23
27
|
@nesting = nesting
|
24
|
-
@index = index
|
25
28
|
@typechecker_enabled = typechecker_enabled
|
26
29
|
|
27
30
|
dispatcher.register(
|
@@ -63,12 +66,17 @@ module RubyLsp
|
|
63
66
|
|
64
67
|
sig { params(node: Prism::CallNode).void }
|
65
68
|
def handle_method_definition(node)
|
66
|
-
return unless self_receiver?(node)
|
67
|
-
|
68
69
|
message = node.message
|
69
70
|
return unless message
|
70
71
|
|
71
|
-
methods =
|
72
|
+
methods = if self_receiver?(node)
|
73
|
+
@index.resolve_method(message, @nesting.join("::"))
|
74
|
+
else
|
75
|
+
# If the method doesn't have a receiver, then we provide a few candidates to jump to
|
76
|
+
# But we don't want to provide too many candidates, as it can be overwhelming
|
77
|
+
@index[message]&.take(MAX_NUMBER_OF_DEFINITION_CANDIDATES_WITHOUT_RECEIVER)
|
78
|
+
end
|
79
|
+
|
72
80
|
return unless methods
|
73
81
|
|
74
82
|
methods.each do |target_method|
|