ruby-lsp 0.14.5 → 0.15.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.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/exe/ruby-lsp +1 -16
- data/exe/ruby-lsp-check +13 -22
- data/lib/ruby_indexer/lib/ruby_indexer/collector.rb +21 -0
- data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +12 -24
- data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +16 -0
- data/lib/ruby_indexer/lib/ruby_indexer/index.rb +3 -2
- 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_indexer/test/index_test.rb +5 -0
- data/lib/ruby_lsp/addon.rb +18 -5
- data/lib/ruby_lsp/base_server.rb +147 -0
- data/lib/ruby_lsp/document.rb +0 -5
- data/lib/ruby_lsp/{requests/support/dependency_detector.rb → global_state.rb} +30 -9
- data/lib/ruby_lsp/internal.rb +2 -1
- data/lib/ruby_lsp/listeners/code_lens.rb +66 -18
- data/lib/ruby_lsp/listeners/completion.rb +13 -14
- data/lib/ruby_lsp/listeners/definition.rb +4 -3
- 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 +4 -3
- 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 +18 -5
- data/lib/ruby_lsp/requests/document_symbol.rb +3 -3
- data/lib/ruby_lsp/requests/hover.rb +9 -5
- data/lib/ruby_lsp/requests/request.rb +51 -0
- data/lib/ruby_lsp/requests/signature_help.rb +4 -3
- data/lib/ruby_lsp/requests/support/common.rb +25 -5
- data/lib/ruby_lsp/requests/support/rubocop_runner.rb +4 -0
- data/lib/ruby_lsp/requests/workspace_symbol.rb +5 -4
- data/lib/ruby_lsp/requests.rb +2 -0
- data/lib/ruby_lsp/server.rb +756 -142
- data/lib/ruby_lsp/store.rb +0 -8
- data/lib/ruby_lsp/test_helper.rb +45 -0
- data/lib/ruby_lsp/utils.rb +68 -33
- metadata +8 -5
- data/lib/ruby_lsp/executor.rb +0 -612
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9749ddfd5f25d0f6e3c0636a070d8977c19af973943e33deba4e49cba9b473d6
|
4
|
+
data.tar.gz: 1c3f663fd6487e8517154a2595be253659e2f2f9bd6a4df23c8b16a91e159c4d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '039fc58aa31af82930cc2852819208fdd9045ed493cf368702f87e9ffd814487bf63fe522124fa3210336cfc72d806b828e2e338114a66913dbd688ed85980c3'
|
7
|
+
data.tar.gz: bb4d62e3ce21fe07cda085f0284b69c356a10694125454b8160bd0c5e87789481b2d4f8036cdb4cdc2b0667c8a78c7e7a1409307722a532122172fcff8c5450c
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.15.0
|
data/exe/ruby-lsp
CHANGED
@@ -68,22 +68,7 @@ if ENV["BUNDLE_GEMFILE"].nil?
|
|
68
68
|
exit exec(env, "bundle exec ruby-lsp #{original_args.join(" ")}")
|
69
69
|
end
|
70
70
|
|
71
|
-
require "
|
72
|
-
|
73
|
-
begin
|
74
|
-
T::Configuration.default_checked_level = :never
|
75
|
-
# Suppresses call validation errors
|
76
|
-
T::Configuration.call_validation_error_handler = ->(*) {}
|
77
|
-
# Suppresses errors caused by T.cast, T.let, T.must, etc.
|
78
|
-
T::Configuration.inline_type_error_handler = ->(*) {}
|
79
|
-
# Suppresses errors caused by incorrect parameter ordering
|
80
|
-
T::Configuration.sig_validation_error_handler = ->(*) {}
|
81
|
-
rescue
|
82
|
-
# Need this rescue so that if another gem has
|
83
|
-
# already set the checked level by the time we
|
84
|
-
# get to it, we don't fail outright.
|
85
|
-
nil
|
86
|
-
end
|
71
|
+
require "ruby_lsp/load_sorbet"
|
87
72
|
|
88
73
|
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
89
74
|
require "ruby_lsp/internal"
|
data/exe/ruby-lsp-check
CHANGED
@@ -3,16 +3,7 @@
|
|
3
3
|
|
4
4
|
# This executable checks if all automatic LSP requests run successfully on every Ruby file under the current directory
|
5
5
|
|
6
|
-
require "
|
7
|
-
|
8
|
-
begin
|
9
|
-
T::Configuration.default_checked_level = :never
|
10
|
-
T::Configuration.call_validation_error_handler = ->(*) {}
|
11
|
-
T::Configuration.inline_type_error_handler = ->(*) {}
|
12
|
-
T::Configuration.sig_validation_error_handler = ->(*) {}
|
13
|
-
rescue
|
14
|
-
nil
|
15
|
-
end
|
6
|
+
require "ruby_lsp/load_sorbet"
|
16
7
|
|
17
8
|
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
18
9
|
require "ruby_lsp/internal"
|
@@ -24,30 +15,30 @@ files = Dir.glob("#{Dir.pwd}/**/*.rb")
|
|
24
15
|
puts "Verifying that all automatic LSP requests execute successfully. This may take a while..."
|
25
16
|
|
26
17
|
errors = {}
|
27
|
-
|
28
|
-
message_queue = Thread::Queue.new
|
29
|
-
RubyLsp::Addon.load_addons(message_queue)
|
30
|
-
executor = RubyLsp::Executor.new(store, message_queue)
|
18
|
+
server = RubyLsp::Server.new(test_mode: true)
|
31
19
|
|
32
20
|
files.each_with_index do |file, index|
|
33
21
|
uri = URI("file://#{file}")
|
34
|
-
|
22
|
+
server.process_message({
|
23
|
+
method: "textDocument/didOpen",
|
24
|
+
params: { textDocument: { uri: uri, text: File.read(file), version: 1 } },
|
25
|
+
})
|
35
26
|
|
36
27
|
# Executing any of the automatic requests will execute all of them, so here we just pick one
|
37
|
-
|
28
|
+
server.process_message({
|
29
|
+
id: 1,
|
38
30
|
method: "textDocument/documentSymbol",
|
39
|
-
params: { textDocument: { uri: uri
|
31
|
+
params: { textDocument: { uri: uri } },
|
40
32
|
})
|
41
33
|
|
42
|
-
|
43
|
-
errors[file] =
|
34
|
+
result = server.pop_response
|
35
|
+
errors[file] = result.message if result.is_a?(RubyLsp::Error)
|
44
36
|
ensure
|
45
|
-
|
37
|
+
server.process_message({ method: "textDocument/didClose", params: { textDocument: { uri: uri } } })
|
38
|
+
server.pop_response
|
46
39
|
print("\033[M\033[0KCompleted #{index + 1}/#{files.length}") unless ENV["CI"]
|
47
40
|
end
|
48
|
-
|
49
41
|
puts "\n"
|
50
|
-
message_queue.close
|
51
42
|
|
52
43
|
# Indexing
|
53
44
|
puts "Verifying that indexing executes successfully. This may take a while..."
|
@@ -152,6 +152,8 @@ module RubyIndexer
|
|
152
152
|
handle_attribute(node, reader: false, writer: true)
|
153
153
|
when :attr_accessor
|
154
154
|
handle_attribute(node, reader: true, writer: true)
|
155
|
+
when :include
|
156
|
+
handle_include(node)
|
155
157
|
end
|
156
158
|
end
|
157
159
|
|
@@ -350,5 +352,24 @@ module RubyIndexer
|
|
350
352
|
@index << Entry::Accessor.new("#{name}=", @file_path, loc, comments, @current_owner) if writer
|
351
353
|
end
|
352
354
|
end
|
355
|
+
|
356
|
+
sig { params(node: Prism::CallNode).void }
|
357
|
+
def handle_include(node)
|
358
|
+
return unless @current_owner
|
359
|
+
|
360
|
+
arguments = node.arguments&.arguments
|
361
|
+
return unless arguments
|
362
|
+
|
363
|
+
names = arguments.filter_map do |node|
|
364
|
+
if node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
|
365
|
+
node.full_name
|
366
|
+
end
|
367
|
+
rescue Prism::ConstantPathNode::DynamicPartsInConstantPathError
|
368
|
+
# TO DO: add MissingNodesInConstantPathError when released in Prism
|
369
|
+
# If a constant path reference is dynamic or missing parts, we can't
|
370
|
+
# index it
|
371
|
+
end
|
372
|
+
@current_owner.included_modules.concat(names)
|
373
|
+
end
|
353
374
|
end
|
354
375
|
end
|
@@ -20,7 +20,7 @@ module RubyIndexer
|
|
20
20
|
def initialize
|
21
21
|
@excluded_gems = T.let(initial_excluded_gems, T::Array[String])
|
22
22
|
@included_gems = T.let([], T::Array[String])
|
23
|
-
@excluded_patterns = T.let([File.join("**", "*_test.rb")], T::Array[String])
|
23
|
+
@excluded_patterns = T.let([File.join("**", "*_test.rb"), File.join("**", "tmp", "**", "*")], T::Array[String])
|
24
24
|
path = Bundler.settings["path"]
|
25
25
|
@excluded_patterns << File.join(File.expand_path(path, Dir.pwd), "**", "*.rb") if path
|
26
26
|
|
@@ -43,20 +43,6 @@ module RubyIndexer
|
|
43
43
|
)
|
44
44
|
end
|
45
45
|
|
46
|
-
sig { void }
|
47
|
-
def load_config
|
48
|
-
return unless File.exist?(".index.yml")
|
49
|
-
|
50
|
-
config = YAML.parse_file(".index.yml")
|
51
|
-
return unless config
|
52
|
-
|
53
|
-
config_hash = config.to_ruby
|
54
|
-
validate_config!(config_hash)
|
55
|
-
apply_config(config_hash)
|
56
|
-
rescue Psych::SyntaxError => e
|
57
|
-
raise e, "Syntax error while loading .index.yml configuration: #{e.message}"
|
58
|
-
end
|
59
|
-
|
60
46
|
sig { returns(T::Array[IndexablePath]) }
|
61
47
|
def indexables
|
62
48
|
excluded_gems = @excluded_gems - @included_gems
|
@@ -158,6 +144,17 @@ module RubyIndexer
|
|
158
144
|
@magic_comment_regex ||= T.let(/^#\s*#{@excluded_magic_comments.join("|")}/, T.nilable(Regexp))
|
159
145
|
end
|
160
146
|
|
147
|
+
sig { params(config: T::Hash[String, T.untyped]).void }
|
148
|
+
def apply_config(config)
|
149
|
+
validate_config!(config)
|
150
|
+
|
151
|
+
@excluded_gems.concat(config["excluded_gems"]) if config["excluded_gems"]
|
152
|
+
@included_gems.concat(config["included_gems"]) if config["included_gems"]
|
153
|
+
@excluded_patterns.concat(config["excluded_patterns"]) if config["excluded_patterns"]
|
154
|
+
@included_patterns.concat(config["included_patterns"]) if config["included_patterns"]
|
155
|
+
@excluded_magic_comments.concat(config["excluded_magic_comments"]) if config["excluded_magic_comments"]
|
156
|
+
end
|
157
|
+
|
161
158
|
private
|
162
159
|
|
163
160
|
sig { params(config: T::Hash[String, T.untyped]).void }
|
@@ -175,15 +172,6 @@ module RubyIndexer
|
|
175
172
|
raise ArgumentError, errors.join("\n") if errors.any?
|
176
173
|
end
|
177
174
|
|
178
|
-
sig { params(config: T::Hash[String, T.untyped]).void }
|
179
|
-
def apply_config(config)
|
180
|
-
@excluded_gems.concat(config["excluded_gems"]) if config["excluded_gems"]
|
181
|
-
@included_gems.concat(config["included_gems"]) if config["included_gems"]
|
182
|
-
@excluded_patterns.concat(config["excluded_patterns"]) if config["excluded_patterns"]
|
183
|
-
@included_patterns.concat(config["included_patterns"]) if config["included_patterns"]
|
184
|
-
@excluded_magic_comments.concat(config["excluded_magic_comments"]) if config["excluded_magic_comments"]
|
185
|
-
end
|
186
|
-
|
187
175
|
sig { returns(T::Array[String]) }
|
188
176
|
def initial_excluded_gems
|
189
177
|
excluded, others = Bundler.definition.dependencies.partition do |dependency|
|
@@ -40,6 +40,22 @@ module RubyIndexer
|
|
40
40
|
|
41
41
|
abstract!
|
42
42
|
|
43
|
+
sig { returns(T::Array[String]) }
|
44
|
+
attr_accessor :included_modules
|
45
|
+
|
46
|
+
sig do
|
47
|
+
params(
|
48
|
+
name: String,
|
49
|
+
file_path: String,
|
50
|
+
location: Prism::Location,
|
51
|
+
comments: T::Array[String],
|
52
|
+
).void
|
53
|
+
end
|
54
|
+
def initialize(name, file_path, location, comments)
|
55
|
+
super(name, file_path, location, comments)
|
56
|
+
@included_modules = T.let([], T::Array[String])
|
57
|
+
end
|
58
|
+
|
43
59
|
sig { returns(String) }
|
44
60
|
def short_name
|
45
61
|
T.must(@name.split("::").last)
|
@@ -191,8 +191,9 @@ module RubyIndexer
|
|
191
191
|
|
192
192
|
require_path = indexable_path.require_path
|
193
193
|
@require_paths_tree.insert(require_path, indexable_path) if require_path
|
194
|
-
rescue Errno::EISDIR
|
195
|
-
# If `path` is a directory, just ignore it and continue indexing
|
194
|
+
rescue Errno::EISDIR, Errno::ENOENT
|
195
|
+
# If `path` is a directory, just ignore it and continue indexing. If the file doesn't exist, then we also ignore
|
196
|
+
# it
|
196
197
|
end
|
197
198
|
|
198
199
|
# Follows aliases in a namespace. The algorithm keeps checking if the name is an alias and then recursively follows
|
@@ -281,5 +281,51 @@ module RubyIndexer
|
|
281
281
|
final_thing = T.must(@index["FinalThing"].first)
|
282
282
|
assert_equal("Something::Baz", final_thing.parent_class)
|
283
283
|
end
|
284
|
+
|
285
|
+
def test_keeping_track_of_included_modules
|
286
|
+
index(<<~RUBY)
|
287
|
+
class Foo
|
288
|
+
# valid syntaxes that we can index
|
289
|
+
include A1
|
290
|
+
self.include A2
|
291
|
+
include A3, A4
|
292
|
+
self.include A5, A6
|
293
|
+
|
294
|
+
# valid syntaxes that we cannot index because of their dynamic nature
|
295
|
+
include some_variable_or_method_call
|
296
|
+
self.include some_variable_or_method_call
|
297
|
+
|
298
|
+
def something
|
299
|
+
include A7 # We should not index this because of this dynamic nature
|
300
|
+
end
|
301
|
+
|
302
|
+
# Valid inner class syntax definition with its own modules included
|
303
|
+
class Qux
|
304
|
+
include Corge
|
305
|
+
self.include Corge
|
306
|
+
include Baz
|
307
|
+
|
308
|
+
include some_variable_or_method_call
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
class ConstantPathReferences
|
313
|
+
include Foo::Bar
|
314
|
+
self.include Foo::Bar2
|
315
|
+
|
316
|
+
include dynamic::Bar
|
317
|
+
include Foo::
|
318
|
+
end
|
319
|
+
RUBY
|
320
|
+
|
321
|
+
foo = T.must(@index["Foo"][0])
|
322
|
+
assert_equal(["A1", "A2", "A3", "A4", "A5", "A6"], foo.included_modules)
|
323
|
+
|
324
|
+
qux = T.must(@index["Foo::Qux"][0])
|
325
|
+
assert_equal(["Corge", "Corge", "Baz"], qux.included_modules)
|
326
|
+
|
327
|
+
constant_path_references = T.must(@index["ConstantPathReferences"][0])
|
328
|
+
assert_equal(["Foo::Bar", "Foo::Bar2"], constant_path_references.included_modules)
|
329
|
+
end
|
284
330
|
end
|
285
331
|
end
|
@@ -10,7 +10,7 @@ module RubyIndexer
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def test_load_configuration_executes_configure_block
|
13
|
-
@config.
|
13
|
+
@config.apply_config({ "excluded_patterns" => ["**/test/fixtures/**/*.rb"] })
|
14
14
|
indexables = @config.indexables
|
15
15
|
|
16
16
|
assert(indexables.none? { |indexable| indexable.full_path.include?("test/fixtures") })
|
@@ -21,7 +21,6 @@ module RubyIndexer
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def test_indexables_only_includes_gem_require_paths
|
24
|
-
@config.load_config
|
25
24
|
indexables = @config.indexables
|
26
25
|
|
27
26
|
Bundler.locked_gems.specs.each do |lazy_spec|
|
@@ -35,7 +34,6 @@ module RubyIndexer
|
|
35
34
|
end
|
36
35
|
|
37
36
|
def test_indexables_does_not_include_default_gem_path_when_in_bundle
|
38
|
-
@config.load_config
|
39
37
|
indexables = @config.indexables
|
40
38
|
|
41
39
|
assert(
|
@@ -44,7 +42,6 @@ module RubyIndexer
|
|
44
42
|
end
|
45
43
|
|
46
44
|
def test_indexables_includes_default_gems
|
47
|
-
@config.load_config
|
48
45
|
indexables = @config.indexables.map(&:full_path)
|
49
46
|
|
50
47
|
assert_includes(indexables, "#{RbConfig::CONFIG["rubylibdir"]}/pathname.rb")
|
@@ -53,7 +50,6 @@ module RubyIndexer
|
|
53
50
|
end
|
54
51
|
|
55
52
|
def test_indexables_includes_project_files
|
56
|
-
@config.load_config
|
57
53
|
indexables = @config.indexables.map(&:full_path)
|
58
54
|
|
59
55
|
Dir.glob("#{Dir.pwd}/lib/**/*.rb").each do |path|
|
@@ -66,7 +62,6 @@ module RubyIndexer
|
|
66
62
|
def test_indexables_avoids_duplicates_if_bundle_path_is_inside_project
|
67
63
|
Bundler.settings.set_global("path", "vendor/bundle")
|
68
64
|
config = Configuration.new
|
69
|
-
config.load_config
|
70
65
|
|
71
66
|
assert_includes(config.instance_variable_get(:@excluded_patterns), "#{Dir.pwd}/vendor/bundle/**/*.rb")
|
72
67
|
ensure
|
@@ -74,7 +69,6 @@ module RubyIndexer
|
|
74
69
|
end
|
75
70
|
|
76
71
|
def test_indexables_does_not_include_gems_own_installed_files
|
77
|
-
@config.load_config
|
78
72
|
indexables = @config.indexables
|
79
73
|
|
80
74
|
assert(
|
@@ -95,17 +89,14 @@ module RubyIndexer
|
|
95
89
|
end
|
96
90
|
|
97
91
|
def test_paths_are_unique
|
98
|
-
@config.load_config
|
99
92
|
indexables = @config.indexables
|
100
93
|
|
101
94
|
assert_equal(indexables.uniq.length, indexables.length)
|
102
95
|
end
|
103
96
|
|
104
97
|
def test_configuration_raises_for_unknown_keys
|
105
|
-
Psych::Nodes::Document.any_instance.expects(:to_ruby).returns({ "unknown_config" => 123 })
|
106
|
-
|
107
98
|
assert_raises(ArgumentError) do
|
108
|
-
@config.
|
99
|
+
@config.apply_config({ "unknown_config" => 123 })
|
109
100
|
end
|
110
101
|
end
|
111
102
|
|
@@ -308,5 +308,10 @@ module RubyIndexer
|
|
308
308
|
|
309
309
|
refute_empty(@index.instance_variable_get(:@entries))
|
310
310
|
end
|
311
|
+
|
312
|
+
def test_index_single_does_not_fail_for_non_existing_file
|
313
|
+
@index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb"))
|
314
|
+
assert_empty(@index.instance_variable_get(:@entries))
|
315
|
+
end
|
311
316
|
end
|
312
317
|
end
|
data/lib/ruby_lsp/addon.rb
CHANGED
@@ -121,22 +121,23 @@ module RubyLsp
|
|
121
121
|
sig do
|
122
122
|
overridable.params(
|
123
123
|
response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::CodeLens],
|
124
|
+
global_state: GlobalState,
|
124
125
|
uri: URI::Generic,
|
125
126
|
dispatcher: Prism::Dispatcher,
|
126
127
|
).void
|
127
128
|
end
|
128
|
-
def create_code_lens_listener(response_builder, uri, dispatcher); end
|
129
|
+
def create_code_lens_listener(response_builder, global_state, uri, dispatcher); end
|
129
130
|
|
130
131
|
# Creates a new Hover listener. This method is invoked on every Hover request
|
131
132
|
sig do
|
132
133
|
overridable.params(
|
133
134
|
response_builder: ResponseBuilders::Hover,
|
135
|
+
global_state: GlobalState,
|
134
136
|
nesting: T::Array[String],
|
135
|
-
index: RubyIndexer::Index,
|
136
137
|
dispatcher: Prism::Dispatcher,
|
137
138
|
).void
|
138
139
|
end
|
139
|
-
def create_hover_listener(response_builder,
|
140
|
+
def create_hover_listener(response_builder, global_state, nesting, dispatcher); end
|
140
141
|
|
141
142
|
# Creates a new DocumentSymbol listener. This method is invoked on every DocumentSymbol request
|
142
143
|
sig do
|
@@ -159,12 +160,24 @@ module RubyLsp
|
|
159
160
|
sig do
|
160
161
|
overridable.params(
|
161
162
|
response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::Location],
|
163
|
+
global_state: GlobalState,
|
162
164
|
uri: URI::Generic,
|
163
165
|
nesting: T::Array[String],
|
164
|
-
index: RubyIndexer::Index,
|
165
166
|
dispatcher: Prism::Dispatcher,
|
166
167
|
).void
|
167
168
|
end
|
168
|
-
def create_definition_listener(response_builder, uri, nesting,
|
169
|
+
def create_definition_listener(response_builder, global_state, uri, nesting, dispatcher); end
|
170
|
+
|
171
|
+
# Creates a new Completion listener. This method is invoked on every Completion request
|
172
|
+
sig do
|
173
|
+
overridable.params(
|
174
|
+
response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::CompletionItem],
|
175
|
+
global_state: GlobalState,
|
176
|
+
nesting: T::Array[String],
|
177
|
+
dispatcher: Prism::Dispatcher,
|
178
|
+
uri: URI::Generic,
|
179
|
+
).void
|
180
|
+
end
|
181
|
+
def create_completion_listener(response_builder, global_state, nesting, dispatcher, uri); end
|
169
182
|
end
|
170
183
|
end
|
@@ -0,0 +1,147 @@
|
|
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
|
+
# We need to process shutdown and exit from the main thread in order to close queues and wait for other threads
|
63
|
+
# to finish. Everything else is pushed into the incoming queue
|
64
|
+
case method
|
65
|
+
when "shutdown"
|
66
|
+
$stderr.puts("Shutting down Ruby LSP...")
|
67
|
+
|
68
|
+
shutdown
|
69
|
+
|
70
|
+
@mutex.synchronize do
|
71
|
+
run_shutdown
|
72
|
+
@writer.write(Result.new(id: message[:id], response: nil).to_hash)
|
73
|
+
end
|
74
|
+
when "exit"
|
75
|
+
@mutex.synchronize do
|
76
|
+
status = @incoming_queue.closed? ? 0 : 1
|
77
|
+
$stderr.puts("Shutdown complete with status #{status}")
|
78
|
+
exit(status)
|
79
|
+
end
|
80
|
+
else
|
81
|
+
@incoming_queue << message
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
sig { void }
|
87
|
+
def run_shutdown
|
88
|
+
@incoming_queue.clear
|
89
|
+
@outgoing_queue.clear
|
90
|
+
@incoming_queue.close
|
91
|
+
@outgoing_queue.close
|
92
|
+
@cancelled_requests.clear
|
93
|
+
|
94
|
+
@worker.join
|
95
|
+
@outgoing_dispatcher.join
|
96
|
+
@store.clear
|
97
|
+
end
|
98
|
+
|
99
|
+
# This method is only intended to be used in tests! Pops the latest response that would be sent to the client
|
100
|
+
sig { returns(T.untyped) }
|
101
|
+
def pop_response
|
102
|
+
@outgoing_queue.pop
|
103
|
+
end
|
104
|
+
|
105
|
+
sig { abstract.params(message: T::Hash[Symbol, T.untyped]).void }
|
106
|
+
def process_message(message); end
|
107
|
+
|
108
|
+
sig { abstract.void }
|
109
|
+
def shutdown; end
|
110
|
+
|
111
|
+
sig { returns(Thread) }
|
112
|
+
def new_worker
|
113
|
+
Thread.new do
|
114
|
+
while (message = T.let(@incoming_queue.pop, T.nilable(T::Hash[Symbol, T.untyped])))
|
115
|
+
id = message[:id]
|
116
|
+
|
117
|
+
# Check if the request was cancelled before trying to process it
|
118
|
+
@mutex.synchronize do
|
119
|
+
if id && @cancelled_requests.include?(id)
|
120
|
+
send_message(Result.new(id: id, response: nil))
|
121
|
+
@cancelled_requests.delete(id)
|
122
|
+
next
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
process_message(message)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
sig { params(message: T.any(Result, Error, Notification, Request)).void }
|
132
|
+
def send_message(message)
|
133
|
+
# When we're shutting down the server, there's a small race condition between closing the thread queues and
|
134
|
+
# finishing remaining requests. We may close the queue in the middle of processing a request, which will then fail
|
135
|
+
# when trying to send a response back
|
136
|
+
return if @outgoing_queue.closed?
|
137
|
+
|
138
|
+
@outgoing_queue << message
|
139
|
+
@current_request_id += 1 if message.is_a?(Request)
|
140
|
+
end
|
141
|
+
|
142
|
+
sig { params(id: Integer).void }
|
143
|
+
def send_empty_response(id)
|
144
|
+
send_message(Result.new(id: id, response: nil))
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
data/lib/ruby_lsp/document.rb
CHANGED
@@ -1,27 +1,41 @@
|
|
1
1
|
# typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
require "singleton"
|
5
|
-
|
6
4
|
module RubyLsp
|
7
|
-
class
|
8
|
-
include Singleton
|
5
|
+
class GlobalState
|
9
6
|
extend T::Sig
|
10
7
|
|
11
8
|
sig { returns(String) }
|
12
|
-
attr_reader :
|
9
|
+
attr_reader :test_library
|
13
10
|
|
14
11
|
sig { returns(String) }
|
15
|
-
|
12
|
+
attr_accessor :formatter
|
16
13
|
|
17
14
|
sig { returns(T::Boolean) }
|
18
15
|
attr_reader :typechecker
|
19
16
|
|
17
|
+
sig { returns(RubyIndexer::Index) }
|
18
|
+
attr_reader :index
|
19
|
+
|
20
20
|
sig { void }
|
21
21
|
def initialize
|
22
|
-
@
|
23
|
-
|
22
|
+
@workspace_uri = T.let(URI::Generic.from_path(path: Dir.pwd), URI::Generic)
|
23
|
+
|
24
|
+
@formatter = T.let(detect_formatter, String)
|
25
|
+
@test_library = T.let(detect_test_library, String)
|
24
26
|
@typechecker = T.let(detect_typechecker, T::Boolean)
|
27
|
+
@index = T.let(RubyIndexer::Index.new, RubyIndexer::Index)
|
28
|
+
end
|
29
|
+
|
30
|
+
sig { params(options: T::Hash[Symbol, T.untyped]).void }
|
31
|
+
def apply_options(options)
|
32
|
+
workspace_uri = options.dig(:workspaceFolders, 0, :uri)
|
33
|
+
@workspace_uri = URI(workspace_uri) if workspace_uri
|
34
|
+
end
|
35
|
+
|
36
|
+
sig { returns(String) }
|
37
|
+
def workspace_path
|
38
|
+
T.must(@workspace_uri.to_standardized_path)
|
25
39
|
end
|
26
40
|
|
27
41
|
sig { returns(String) }
|
@@ -63,8 +77,15 @@ module RubyLsp
|
|
63
77
|
def detect_typechecker
|
64
78
|
return false if ENV["RUBY_LSP_BYPASS_TYPECHECKER"]
|
65
79
|
|
80
|
+
# We can't read the env from within `Bundle.with_original_env` so we need to set it here.
|
81
|
+
ruby_lsp_env_is_test = (ENV["RUBY_LSP_ENV"] == "test")
|
66
82
|
Bundler.with_original_env do
|
67
|
-
Bundler.locked_gems.specs.any? { |spec| spec.name == "sorbet-static" }
|
83
|
+
sorbet_static_detected = Bundler.locked_gems.specs.any? { |spec| spec.name == "sorbet-static" }
|
84
|
+
# Don't show message while running tests, since it's noisy
|
85
|
+
if sorbet_static_detected && !ruby_lsp_env_is_test
|
86
|
+
$stderr.puts("Ruby LSP detected this is a Sorbet project so will defer to Sorbet LSP for some functionality")
|
87
|
+
end
|
88
|
+
sorbet_static_detected
|
68
89
|
end
|
69
90
|
rescue Bundler::GemfileNotFound
|
70
91
|
false
|
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"
|