ruby-lsp 0.17.14 → 0.17.16
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/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +28 -9
- data/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +5 -3
- data/lib/ruby_indexer/test/classes_and_modules_test.rb +21 -0
- data/lib/ruby_indexer/test/configuration_test.rb +41 -7
- data/lib/ruby_lsp/document.rb +16 -2
- data/lib/ruby_lsp/listeners/completion.rb +1 -1
- data/lib/ruby_lsp/listeners/definition.rb +8 -5
- data/lib/ruby_lsp/listeners/inlay_hints.rb +11 -0
- data/lib/ruby_lsp/listeners/semantic_highlighting.rb +12 -137
- data/lib/ruby_lsp/requests/diagnostics.rb +3 -1
- data/lib/ruby_lsp/requests/semantic_highlighting.rb +113 -6
- data/lib/ruby_lsp/requests/support/common.rb +0 -9
- data/lib/ruby_lsp/response_builders/semantic_highlighting.rb +4 -4
- data/lib/ruby_lsp/server.rb +82 -20
- data/lib/ruby_lsp/setup_bundler.rb +4 -4
- data/lib/ruby_lsp/store.rb +1 -1
- data/lib/ruby_lsp/test_helper.rb +1 -1
- data/lib/ruby_lsp/type_inferrer.rb +38 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fc4416ea8876d8207b6c1955cd47af5f3312a0f0dc3644563bfd35138a903535
|
4
|
+
data.tar.gz: 5961dd8ca49539c20049509c0afed8b2442c3d16ec12d1fb84c9e45dbd027760
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4b77547221c1750871ff6e1a3635b0ae6f945b86862f484d20eb7587a1feb9830e1d1d7770f16c2e7efeb70e6a953529041f8acc8fe3d7c30940687d20563f96
|
7
|
+
data.tar.gz: 1093a1a6ac45e8627f26a6c9dbb0017079d2804904fe9a4303e3698d4131c6eeedf2bbafac93e6475f676860624b9c53d482c52cfe8380e67ed05ce525e99957
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.17.
|
1
|
+
0.17.16
|
@@ -16,15 +16,24 @@ module RubyIndexer
|
|
16
16
|
T::Hash[String, T.untyped],
|
17
17
|
)
|
18
18
|
|
19
|
+
sig { params(workspace_path: String).void }
|
20
|
+
attr_writer :workspace_path
|
21
|
+
|
19
22
|
sig { void }
|
20
23
|
def initialize
|
24
|
+
@workspace_path = T.let(Dir.pwd, String)
|
21
25
|
@excluded_gems = T.let(initial_excluded_gems, T::Array[String])
|
22
26
|
@included_gems = T.let([], T::Array[String])
|
23
|
-
@excluded_patterns = T.let([File.join("**", "*_test.rb"), File.join("
|
27
|
+
@excluded_patterns = T.let([File.join("**", "*_test.rb"), File.join("tmp", "**", "*")], T::Array[String])
|
28
|
+
|
24
29
|
path = Bundler.settings["path"]
|
25
|
-
|
30
|
+
if path
|
31
|
+
# Substitute Windows backslashes into forward slashes, which are used in glob patterns
|
32
|
+
glob = path.gsub(/[\\]+/, "/")
|
33
|
+
@excluded_patterns << File.join(glob, "**", "*.rb")
|
34
|
+
end
|
26
35
|
|
27
|
-
@included_patterns = T.let([File.join(
|
36
|
+
@included_patterns = T.let([File.join("**", "*.rb")], T::Array[String])
|
28
37
|
@excluded_magic_comments = T.let(
|
29
38
|
[
|
30
39
|
"frozen_string_literal:",
|
@@ -55,12 +64,12 @@ module RubyIndexer
|
|
55
64
|
indexables = @included_patterns.flat_map do |pattern|
|
56
65
|
load_path_entry = T.let(nil, T.nilable(String))
|
57
66
|
|
58
|
-
Dir.glob(pattern, File::FNM_PATHNAME | File::FNM_EXTGLOB).map! do |path|
|
67
|
+
Dir.glob(File.join(@workspace_path, pattern), File::FNM_PATHNAME | File::FNM_EXTGLOB).map! do |path|
|
59
68
|
path = File.expand_path(path)
|
60
69
|
# All entries for the same pattern match the same $LOAD_PATH entry. Since searching the $LOAD_PATH for every
|
61
70
|
# entry is expensive, we memoize it until we find a path that doesn't belong to that $LOAD_PATH. This happens
|
62
|
-
# on repositories that define multiple gems, like Rails. All frameworks are defined inside the
|
63
|
-
# each one of them belongs to a different $LOAD_PATH entry
|
71
|
+
# on repositories that define multiple gems, like Rails. All frameworks are defined inside the current
|
72
|
+
# workspace directory, but each one of them belongs to a different $LOAD_PATH entry
|
64
73
|
if load_path_entry.nil? || !path.start_with?(load_path_entry)
|
65
74
|
load_path_entry = $LOAD_PATH.find { |load_path| path.start_with?(load_path) }
|
66
75
|
end
|
@@ -69,9 +78,19 @@ module RubyIndexer
|
|
69
78
|
end
|
70
79
|
end
|
71
80
|
|
81
|
+
# If the patterns are relative, we make it relative to the workspace path. If they are absolute, then we shouldn't
|
82
|
+
# concatenate anything
|
83
|
+
excluded_patterns = @excluded_patterns.map do |pattern|
|
84
|
+
if File.absolute_path?(pattern)
|
85
|
+
pattern
|
86
|
+
else
|
87
|
+
File.join(@workspace_path, pattern)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
72
91
|
# Remove user specified patterns
|
73
92
|
indexables.reject! do |indexable|
|
74
|
-
|
93
|
+
excluded_patterns.any? do |pattern|
|
75
94
|
File.fnmatch?(pattern, indexable.full_path, File::FNM_PATHNAME | File::FNM_EXTGLOB)
|
76
95
|
end
|
77
96
|
end
|
@@ -122,7 +141,7 @@ module RubyIndexer
|
|
122
141
|
# When working on a gem, it will be included in the locked_gems list. Since these are the project's own files,
|
123
142
|
# we have already included and handled exclude patterns for it and should not re-include or it'll lead to
|
124
143
|
# duplicates or accidentally ignoring exclude patterns
|
125
|
-
next if spec.full_gem_path ==
|
144
|
+
next if spec.full_gem_path == @workspace_path
|
126
145
|
|
127
146
|
indexables.concat(
|
128
147
|
spec.require_paths.flat_map do |require_path|
|
@@ -185,7 +204,7 @@ module RubyIndexer
|
|
185
204
|
# If the dependency is prerelease, `to_spec` may return `nil` due to a bug in older version of Bundler/RubyGems:
|
186
205
|
# https://github.com/Shopify/ruby-lsp/issues/1246
|
187
206
|
this_gem = Bundler.definition.dependencies.find do |d|
|
188
|
-
d.to_spec&.full_gem_path ==
|
207
|
+
d.to_spec&.full_gem_path == @workspace_path
|
189
208
|
rescue Gem::MissingSpecError
|
190
209
|
false
|
191
210
|
end
|
@@ -152,16 +152,17 @@ module RubyIndexer
|
|
152
152
|
|
153
153
|
if current_owner
|
154
154
|
expression = node.expression
|
155
|
-
|
155
|
+
name = (expression.is_a?(Prism::SelfNode) ? "<Class:#{@stack.last}>" : "<Class:#{expression.slice}>")
|
156
|
+
real_nesting = actual_nesting(name)
|
156
157
|
|
157
|
-
existing_entries = T.cast(@index[
|
158
|
+
existing_entries = T.cast(@index[real_nesting.join("::")], T.nilable(T::Array[Entry::SingletonClass]))
|
158
159
|
|
159
160
|
if existing_entries
|
160
161
|
entry = T.must(existing_entries.first)
|
161
162
|
entry.update_singleton_information(node.location, expression.location, collect_comments(node))
|
162
163
|
else
|
163
164
|
entry = Entry::SingletonClass.new(
|
164
|
-
|
165
|
+
real_nesting,
|
165
166
|
@file_path,
|
166
167
|
node.location,
|
167
168
|
expression.location,
|
@@ -172,6 +173,7 @@ module RubyIndexer
|
|
172
173
|
end
|
173
174
|
|
174
175
|
@owner_stack << entry
|
176
|
+
@stack << name
|
175
177
|
end
|
176
178
|
end
|
177
179
|
|
@@ -564,6 +564,27 @@ module RubyIndexer
|
|
564
564
|
assert_entry("Foo::Bar", Entry::Class, "/fake/path/foo.rb:1-2:2-5")
|
565
565
|
end
|
566
566
|
|
567
|
+
def test_indexing_singletons_inside_top_level_references
|
568
|
+
index(<<~RUBY)
|
569
|
+
module ::Foo
|
570
|
+
class Bar
|
571
|
+
class << self
|
572
|
+
end
|
573
|
+
end
|
574
|
+
end
|
575
|
+
RUBY
|
576
|
+
|
577
|
+
# We want to explicitly verify that we didn't introduce the leading `::` by accident, but `Index#[]` deletes the
|
578
|
+
# prefix when we use `refute_entry`
|
579
|
+
entries = @index.instance_variable_get(:@entries)
|
580
|
+
refute(entries.key?("::Foo"))
|
581
|
+
refute(entries.key?("::Foo::Bar"))
|
582
|
+
refute(entries.key?("::Foo::Bar::<Class:Bar>"))
|
583
|
+
assert_entry("Foo", Entry::Module, "/fake/path/foo.rb:0-0:5-3")
|
584
|
+
assert_entry("Foo::Bar", Entry::Class, "/fake/path/foo.rb:1-2:4-5")
|
585
|
+
assert_entry("Foo::Bar::<Class:Bar>", Entry::SingletonClass, "/fake/path/foo.rb:2-4:3-7")
|
586
|
+
end
|
587
|
+
|
567
588
|
def test_indexing_namespaces_inside_nested_top_level_references
|
568
589
|
index(<<~RUBY)
|
569
590
|
class Baz
|
@@ -7,10 +7,12 @@ module RubyIndexer
|
|
7
7
|
class ConfigurationTest < Minitest::Test
|
8
8
|
def setup
|
9
9
|
@config = Configuration.new
|
10
|
+
@workspace_path = File.expand_path(File.join("..", "..", ".."), __dir__)
|
11
|
+
@config.workspace_path = @workspace_path
|
10
12
|
end
|
11
13
|
|
12
14
|
def test_load_configuration_executes_configure_block
|
13
|
-
@config.apply_config({ "excluded_patterns" => ["**/
|
15
|
+
@config.apply_config({ "excluded_patterns" => ["**/fixtures/**/*.rb"] })
|
14
16
|
indexables = @config.indexables
|
15
17
|
|
16
18
|
assert(indexables.none? { |indexable| indexable.full_path.include?("test/fixtures") })
|
@@ -25,7 +27,7 @@ module RubyIndexer
|
|
25
27
|
indexables = @config.indexables
|
26
28
|
|
27
29
|
# All paths should be expanded
|
28
|
-
assert(indexables.
|
30
|
+
assert(indexables.all? { |indexable| File.absolute_path?(indexable.full_path) })
|
29
31
|
end
|
30
32
|
|
31
33
|
def test_indexables_only_includes_gem_require_paths
|
@@ -71,17 +73,20 @@ module RubyIndexer
|
|
71
73
|
Bundler.settings.temporary(path: "vendor/bundle") do
|
72
74
|
config = Configuration.new
|
73
75
|
|
74
|
-
assert_includes(config.instance_variable_get(:@excluded_patterns), "
|
76
|
+
assert_includes(config.instance_variable_get(:@excluded_patterns), "vendor/bundle/**/*.rb")
|
75
77
|
end
|
76
78
|
end
|
77
79
|
|
78
80
|
def test_indexables_does_not_include_gems_own_installed_files
|
79
81
|
indexables = @config.indexables
|
82
|
+
indexables_inside_bundled_lsp = indexables.select do |indexable|
|
83
|
+
indexable.full_path.start_with?(Bundler.bundle_path.join("gems", "ruby-lsp").to_s)
|
84
|
+
end
|
80
85
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
86
|
+
assert_empty(
|
87
|
+
indexables_inside_bundled_lsp,
|
88
|
+
"Indexables should not include files from the gem currently being worked on. " \
|
89
|
+
"Included: #{indexables_inside_bundled_lsp.map(&:full_path)}",
|
85
90
|
)
|
86
91
|
end
|
87
92
|
|
@@ -126,5 +131,34 @@ module RubyIndexer
|
|
126
131
|
assert_match(regex, comment)
|
127
132
|
end
|
128
133
|
end
|
134
|
+
|
135
|
+
def test_indexables_respect_given_workspace_path
|
136
|
+
Dir.mktmpdir do |dir|
|
137
|
+
FileUtils.mkdir(File.join(dir, "ignore"))
|
138
|
+
FileUtils.touch(File.join(dir, "ignore", "file0.rb"))
|
139
|
+
FileUtils.touch(File.join(dir, "file1.rb"))
|
140
|
+
FileUtils.touch(File.join(dir, "file2.rb"))
|
141
|
+
|
142
|
+
@config.apply_config({ "excluded_patterns" => ["ignore/**/*.rb"] })
|
143
|
+
@config.workspace_path = dir
|
144
|
+
indexables = @config.indexables
|
145
|
+
|
146
|
+
assert(indexables.none? { |indexable| indexable.full_path.start_with?(File.join(dir, "ignore")) })
|
147
|
+
|
148
|
+
# After switching the workspace path, all indexables will be found in one of these places:
|
149
|
+
# - The new workspace path
|
150
|
+
# - The Ruby LSP's own code (because Bundler is requiring the dependency from source)
|
151
|
+
# - Bundled gems
|
152
|
+
# - Default gems
|
153
|
+
assert(
|
154
|
+
indexables.all? do |i|
|
155
|
+
i.full_path.start_with?(dir) ||
|
156
|
+
i.full_path.start_with?(File.join(Dir.pwd, "lib")) ||
|
157
|
+
i.full_path.start_with?(Bundler.bundle_path.to_s) ||
|
158
|
+
i.full_path.start_with?(RbConfig::CONFIG["rubylibdir"])
|
159
|
+
end,
|
160
|
+
)
|
161
|
+
end
|
162
|
+
end
|
129
163
|
end
|
130
164
|
end
|
data/lib/ruby_lsp/document.rb
CHANGED
@@ -17,6 +17,11 @@ module RubyLsp
|
|
17
17
|
|
18
18
|
ParseResultType = type_member
|
19
19
|
|
20
|
+
# This maximum number of characters for providing expensive features, like semantic highlighting and diagnostics.
|
21
|
+
# This is the same number used by the TypeScript extension in VS Code
|
22
|
+
MAXIMUM_CHARACTERS_FOR_EXPENSIVE_FEATURES = 100_000
|
23
|
+
EMPTY_CACHE = T.let(Object.new.freeze, Object)
|
24
|
+
|
20
25
|
abstract!
|
21
26
|
|
22
27
|
sig { returns(ParseResultType) }
|
@@ -34,9 +39,13 @@ module RubyLsp
|
|
34
39
|
sig { returns(Encoding) }
|
35
40
|
attr_reader :encoding
|
36
41
|
|
42
|
+
sig { returns(T.any(Interface::SemanticTokens, Object)) }
|
43
|
+
attr_accessor :semantic_tokens
|
44
|
+
|
37
45
|
sig { params(source: String, version: Integer, uri: URI::Generic, encoding: Encoding).void }
|
38
46
|
def initialize(source:, version:, uri:, encoding: Encoding::UTF_8)
|
39
|
-
@cache = T.let(
|
47
|
+
@cache = T.let(Hash.new(EMPTY_CACHE), T::Hash[String, T.untyped])
|
48
|
+
@semantic_tokens = T.let(EMPTY_CACHE, T.any(Interface::SemanticTokens, Object))
|
40
49
|
@encoding = T.let(encoding, Encoding)
|
41
50
|
@source = T.let(source, String)
|
42
51
|
@version = T.let(version, Integer)
|
@@ -63,7 +72,7 @@ module RubyLsp
|
|
63
72
|
end
|
64
73
|
def cache_fetch(request_name, &block)
|
65
74
|
cached = @cache[request_name]
|
66
|
-
return cached if cached
|
75
|
+
return cached if cached != EMPTY_CACHE
|
67
76
|
|
68
77
|
result = block.call(self)
|
69
78
|
@cache[request_name] = result
|
@@ -108,6 +117,11 @@ module RubyLsp
|
|
108
117
|
Scanner.new(@source, @encoding)
|
109
118
|
end
|
110
119
|
|
120
|
+
sig { returns(T::Boolean) }
|
121
|
+
def past_expensive_limit?
|
122
|
+
@source.length > MAXIMUM_CHARACTERS_FOR_EXPENSIVE_FEATURES
|
123
|
+
end
|
124
|
+
|
111
125
|
class Scanner
|
112
126
|
extend T::Sig
|
113
127
|
|
@@ -366,7 +366,7 @@ module RubyLsp
|
|
366
366
|
|
367
367
|
return unless range
|
368
368
|
|
369
|
-
guessed_type = type.name
|
369
|
+
guessed_type = type.is_a?(TypeInferrer::GuessedType) && type.name
|
370
370
|
|
371
371
|
@index.method_completion_candidates(method_name, type.name).each do |entry|
|
372
372
|
entry_name = entry.name
|
@@ -208,10 +208,13 @@ module RubyLsp
|
|
208
208
|
def handle_method_definition(message, receiver_type, inherited_only: false)
|
209
209
|
methods = if receiver_type
|
210
210
|
@index.resolve_method(message, receiver_type.name, inherited_only: inherited_only)
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
211
|
+
end
|
212
|
+
|
213
|
+
# If the method doesn't have a receiver, or the guessed receiver doesn't have any matched candidates,
|
214
|
+
# then we provide a few candidates to jump to
|
215
|
+
# But we don't want to provide too many candidates, as it can be overwhelming
|
216
|
+
if receiver_type.nil? || (receiver_type.is_a?(TypeInferrer::GuessedType) && methods.nil?)
|
217
|
+
methods = @index[message]&.take(MAX_NUMBER_OF_DEFINITION_CANDIDATES_WITHOUT_RECEIVER)
|
215
218
|
end
|
216
219
|
|
217
220
|
return unless methods
|
@@ -250,7 +253,7 @@ module RubyLsp
|
|
250
253
|
when :require_relative
|
251
254
|
required_file = "#{node.content}.rb"
|
252
255
|
path = @uri.to_standardized_path
|
253
|
-
current_folder = path ? Pathname.new(CGI.unescape(path)).dirname :
|
256
|
+
current_folder = path ? Pathname.new(CGI.unescape(path)).dirname : @global_state.workspace_path
|
254
257
|
candidate = File.expand_path(File.join(current_folder, required_file))
|
255
258
|
|
256
259
|
@response_builder << Interface::Location.new(
|
@@ -69,6 +69,17 @@ module RubyLsp
|
|
69
69
|
tooltip: tooltip,
|
70
70
|
)
|
71
71
|
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
sig { params(node: T.nilable(Prism::Node), range: T.nilable(T::Range[Integer])).returns(T::Boolean) }
|
76
|
+
def visible?(node, range)
|
77
|
+
return true if range.nil?
|
78
|
+
return false if node.nil?
|
79
|
+
|
80
|
+
loc = node.location
|
81
|
+
range.cover?(loc.start_line - 1) && range.cover?(loc.end_line - 1)
|
82
|
+
end
|
72
83
|
end
|
73
84
|
end
|
74
85
|
end
|
@@ -14,7 +14,7 @@ module RubyLsp
|
|
14
14
|
Kernel.methods(false),
|
15
15
|
Bundler::Dsl.instance_methods(false),
|
16
16
|
Module.private_instance_methods(false),
|
17
|
-
].flatten.map(&:to_s),
|
17
|
+
].flatten.map(&:to_s).freeze,
|
18
18
|
T::Array[String],
|
19
19
|
)
|
20
20
|
|
@@ -22,12 +22,10 @@ module RubyLsp
|
|
22
22
|
params(
|
23
23
|
dispatcher: Prism::Dispatcher,
|
24
24
|
response_builder: ResponseBuilders::SemanticHighlighting,
|
25
|
-
range: T.nilable(T::Range[Integer]),
|
26
25
|
).void
|
27
26
|
end
|
28
|
-
def initialize(dispatcher, response_builder
|
27
|
+
def initialize(dispatcher, response_builder)
|
29
28
|
@response_builder = response_builder
|
30
|
-
@range = range
|
31
29
|
@special_methods = T.let(nil, T.nilable(T::Array[String]))
|
32
30
|
@current_scope = T.let(ParameterScope.new, ParameterScope)
|
33
31
|
@inside_regex_capture = T.let(false, T::Boolean)
|
@@ -43,7 +41,6 @@ module RubyLsp
|
|
43
41
|
:on_block_node_leave,
|
44
42
|
:on_self_node_enter,
|
45
43
|
:on_module_node_enter,
|
46
|
-
:on_local_variable_write_node_enter,
|
47
44
|
:on_local_variable_read_node_enter,
|
48
45
|
:on_block_parameter_node_enter,
|
49
46
|
:on_required_keyword_parameter_node_enter,
|
@@ -52,16 +49,6 @@ module RubyLsp
|
|
52
49
|
:on_optional_parameter_node_enter,
|
53
50
|
:on_required_parameter_node_enter,
|
54
51
|
:on_rest_parameter_node_enter,
|
55
|
-
:on_constant_read_node_enter,
|
56
|
-
:on_constant_write_node_enter,
|
57
|
-
:on_constant_and_write_node_enter,
|
58
|
-
:on_constant_operator_write_node_enter,
|
59
|
-
:on_constant_or_write_node_enter,
|
60
|
-
:on_constant_target_node_enter,
|
61
|
-
:on_constant_path_node_enter,
|
62
|
-
:on_local_variable_and_write_node_enter,
|
63
|
-
:on_local_variable_operator_write_node_enter,
|
64
|
-
:on_local_variable_or_write_node_enter,
|
65
52
|
:on_local_variable_target_node_enter,
|
66
53
|
:on_block_local_variable_node_enter,
|
67
54
|
:on_match_write_node_enter,
|
@@ -74,7 +61,6 @@ module RubyLsp
|
|
74
61
|
sig { params(node: Prism::CallNode).void }
|
75
62
|
def on_call_node_enter(node)
|
76
63
|
return if @inside_implicit_node
|
77
|
-
return unless visible?(node, @range)
|
78
64
|
|
79
65
|
message = node.message
|
80
66
|
return unless message
|
@@ -85,8 +71,14 @@ module RubyLsp
|
|
85
71
|
return if message == "=~"
|
86
72
|
return if special_method?(message)
|
87
73
|
|
88
|
-
|
89
|
-
|
74
|
+
if Requests::Support::Sorbet.annotation?(node)
|
75
|
+
@response_builder.add_token(T.must(node.message_loc), :type)
|
76
|
+
elsif !node.receiver && !node.opening_loc
|
77
|
+
# If the node has a receiver, then the syntax is not ambiguous and semantic highlighting is not necessary to
|
78
|
+
# determine that the token is a method call. The only ambiguous case is method calls with implicit self
|
79
|
+
# receiver and no parenthesis, which may be confused with local variables
|
80
|
+
@response_builder.add_token(T.must(node.message_loc), :method)
|
81
|
+
end
|
90
82
|
end
|
91
83
|
|
92
84
|
sig { params(node: Prism::MatchWriteNode).void }
|
@@ -104,55 +96,9 @@ module RubyLsp
|
|
104
96
|
@inside_regex_capture = true if node.call.message == "=~"
|
105
97
|
end
|
106
98
|
|
107
|
-
sig { params(node: Prism::ConstantReadNode).void }
|
108
|
-
def on_constant_read_node_enter(node)
|
109
|
-
return if @inside_implicit_node
|
110
|
-
return unless visible?(node, @range)
|
111
|
-
|
112
|
-
@response_builder.add_token(node.location, :namespace)
|
113
|
-
end
|
114
|
-
|
115
|
-
sig { params(node: Prism::ConstantWriteNode).void }
|
116
|
-
def on_constant_write_node_enter(node)
|
117
|
-
return unless visible?(node, @range)
|
118
|
-
|
119
|
-
@response_builder.add_token(node.name_loc, :namespace)
|
120
|
-
end
|
121
|
-
|
122
|
-
sig { params(node: Prism::ConstantAndWriteNode).void }
|
123
|
-
def on_constant_and_write_node_enter(node)
|
124
|
-
return unless visible?(node, @range)
|
125
|
-
|
126
|
-
@response_builder.add_token(node.name_loc, :namespace)
|
127
|
-
end
|
128
|
-
|
129
|
-
sig { params(node: Prism::ConstantOperatorWriteNode).void }
|
130
|
-
def on_constant_operator_write_node_enter(node)
|
131
|
-
return unless visible?(node, @range)
|
132
|
-
|
133
|
-
@response_builder.add_token(node.name_loc, :namespace)
|
134
|
-
end
|
135
|
-
|
136
|
-
sig { params(node: Prism::ConstantOrWriteNode).void }
|
137
|
-
def on_constant_or_write_node_enter(node)
|
138
|
-
return unless visible?(node, @range)
|
139
|
-
|
140
|
-
@response_builder.add_token(node.name_loc, :namespace)
|
141
|
-
end
|
142
|
-
|
143
|
-
sig { params(node: Prism::ConstantTargetNode).void }
|
144
|
-
def on_constant_target_node_enter(node)
|
145
|
-
return unless visible?(node, @range)
|
146
|
-
|
147
|
-
@response_builder.add_token(node.location, :namespace)
|
148
|
-
end
|
149
|
-
|
150
99
|
sig { params(node: Prism::DefNode).void }
|
151
100
|
def on_def_node_enter(node)
|
152
101
|
@current_scope = ParameterScope.new(@current_scope)
|
153
|
-
return unless visible?(node, @range)
|
154
|
-
|
155
|
-
@response_builder.add_token(node.name_loc, :method, [:declaration])
|
156
102
|
end
|
157
103
|
|
158
104
|
sig { params(node: Prism::DefNode).void }
|
@@ -184,77 +130,43 @@ module RubyLsp
|
|
184
130
|
sig { params(node: Prism::RequiredKeywordParameterNode).void }
|
185
131
|
def on_required_keyword_parameter_node_enter(node)
|
186
132
|
@current_scope << node.name
|
187
|
-
return unless visible?(node, @range)
|
188
|
-
|
189
|
-
location = node.name_loc
|
190
|
-
@response_builder.add_token(location.copy(length: location.length - 1), :parameter)
|
191
133
|
end
|
192
134
|
|
193
135
|
sig { params(node: Prism::OptionalKeywordParameterNode).void }
|
194
136
|
def on_optional_keyword_parameter_node_enter(node)
|
195
137
|
@current_scope << node.name
|
196
|
-
return unless visible?(node, @range)
|
197
|
-
|
198
|
-
location = node.name_loc
|
199
|
-
@response_builder.add_token(location.copy(length: location.length - 1), :parameter)
|
200
138
|
end
|
201
139
|
|
202
140
|
sig { params(node: Prism::KeywordRestParameterNode).void }
|
203
141
|
def on_keyword_rest_parameter_node_enter(node)
|
204
142
|
name = node.name
|
205
|
-
|
206
|
-
if name
|
207
|
-
@current_scope << name.to_sym
|
208
|
-
|
209
|
-
@response_builder.add_token(T.must(node.name_loc), :parameter) if visible?(node, @range)
|
210
|
-
end
|
143
|
+
@current_scope << name.to_sym if name
|
211
144
|
end
|
212
145
|
|
213
146
|
sig { params(node: Prism::OptionalParameterNode).void }
|
214
147
|
def on_optional_parameter_node_enter(node)
|
215
148
|
@current_scope << node.name
|
216
|
-
return unless visible?(node, @range)
|
217
|
-
|
218
|
-
@response_builder.add_token(node.name_loc, :parameter)
|
219
149
|
end
|
220
150
|
|
221
151
|
sig { params(node: Prism::RequiredParameterNode).void }
|
222
152
|
def on_required_parameter_node_enter(node)
|
223
153
|
@current_scope << node.name
|
224
|
-
return unless visible?(node, @range)
|
225
|
-
|
226
|
-
@response_builder.add_token(node.location, :parameter)
|
227
154
|
end
|
228
155
|
|
229
156
|
sig { params(node: Prism::RestParameterNode).void }
|
230
157
|
def on_rest_parameter_node_enter(node)
|
231
158
|
name = node.name
|
232
|
-
|
233
|
-
if name
|
234
|
-
@current_scope << name.to_sym
|
235
|
-
|
236
|
-
@response_builder.add_token(T.must(node.name_loc), :parameter) if visible?(node, @range)
|
237
|
-
end
|
159
|
+
@current_scope << name.to_sym if name
|
238
160
|
end
|
239
161
|
|
240
162
|
sig { params(node: Prism::SelfNode).void }
|
241
163
|
def on_self_node_enter(node)
|
242
|
-
return unless visible?(node, @range)
|
243
|
-
|
244
164
|
@response_builder.add_token(node.location, :variable, [:default_library])
|
245
165
|
end
|
246
166
|
|
247
|
-
sig { params(node: Prism::LocalVariableWriteNode).void }
|
248
|
-
def on_local_variable_write_node_enter(node)
|
249
|
-
return unless visible?(node, @range)
|
250
|
-
|
251
|
-
@response_builder.add_token(node.name_loc, @current_scope.type_for(node.name))
|
252
|
-
end
|
253
|
-
|
254
167
|
sig { params(node: Prism::LocalVariableReadNode).void }
|
255
168
|
def on_local_variable_read_node_enter(node)
|
256
169
|
return if @inside_implicit_node
|
257
|
-
return unless visible?(node, @range)
|
258
170
|
|
259
171
|
# Numbered parameters
|
260
172
|
if /_\d+/.match?(node.name)
|
@@ -265,27 +177,6 @@ module RubyLsp
|
|
265
177
|
@response_builder.add_token(node.location, @current_scope.type_for(node.name))
|
266
178
|
end
|
267
179
|
|
268
|
-
sig { params(node: Prism::LocalVariableAndWriteNode).void }
|
269
|
-
def on_local_variable_and_write_node_enter(node)
|
270
|
-
return unless visible?(node, @range)
|
271
|
-
|
272
|
-
@response_builder.add_token(node.name_loc, @current_scope.type_for(node.name))
|
273
|
-
end
|
274
|
-
|
275
|
-
sig { params(node: Prism::LocalVariableOperatorWriteNode).void }
|
276
|
-
def on_local_variable_operator_write_node_enter(node)
|
277
|
-
return unless visible?(node, @range)
|
278
|
-
|
279
|
-
@response_builder.add_token(node.name_loc, @current_scope.type_for(node.name))
|
280
|
-
end
|
281
|
-
|
282
|
-
sig { params(node: Prism::LocalVariableOrWriteNode).void }
|
283
|
-
def on_local_variable_or_write_node_enter(node)
|
284
|
-
return unless visible?(node, @range)
|
285
|
-
|
286
|
-
@response_builder.add_token(node.name_loc, @current_scope.type_for(node.name))
|
287
|
-
end
|
288
|
-
|
289
180
|
sig { params(node: Prism::LocalVariableTargetNode).void }
|
290
181
|
def on_local_variable_target_node_enter(node)
|
291
182
|
# If we're inside a regex capture, Prism will add LocalVariableTarget nodes for each captured variable.
|
@@ -294,15 +185,11 @@ module RubyLsp
|
|
294
185
|
# prevent pushing local variable target tokens. See https://github.com/ruby/prism/issues/1912
|
295
186
|
return if @inside_regex_capture
|
296
187
|
|
297
|
-
return unless visible?(node, @range)
|
298
|
-
|
299
188
|
@response_builder.add_token(node.location, @current_scope.type_for(node.name))
|
300
189
|
end
|
301
190
|
|
302
191
|
sig { params(node: Prism::ClassNode).void }
|
303
192
|
def on_class_node_enter(node)
|
304
|
-
return unless visible?(node, @range)
|
305
|
-
|
306
193
|
constant_path = node.constant_path
|
307
194
|
|
308
195
|
if constant_path.is_a?(Prism::ConstantReadNode)
|
@@ -342,8 +229,6 @@ module RubyLsp
|
|
342
229
|
|
343
230
|
sig { params(node: Prism::ModuleNode).void }
|
344
231
|
def on_module_node_enter(node)
|
345
|
-
return unless visible?(node, @range)
|
346
|
-
|
347
232
|
constant_path = node.constant_path
|
348
233
|
|
349
234
|
if constant_path.is_a?(Prism::ConstantReadNode)
|
@@ -365,8 +250,6 @@ module RubyLsp
|
|
365
250
|
|
366
251
|
sig { params(node: Prism::ImplicitNode).void }
|
367
252
|
def on_implicit_node_enter(node)
|
368
|
-
return unless visible?(node, @range)
|
369
|
-
|
370
253
|
@inside_implicit_node = true
|
371
254
|
end
|
372
255
|
|
@@ -375,14 +258,6 @@ module RubyLsp
|
|
375
258
|
@inside_implicit_node = false
|
376
259
|
end
|
377
260
|
|
378
|
-
sig { params(node: Prism::ConstantPathNode).void }
|
379
|
-
def on_constant_path_node_enter(node)
|
380
|
-
return if @inside_implicit_node
|
381
|
-
return unless visible?(node, @range)
|
382
|
-
|
383
|
-
@response_builder.add_token(node.name_loc, :namespace)
|
384
|
-
end
|
385
|
-
|
386
261
|
private
|
387
262
|
|
388
263
|
# Textmate provides highlighting for a subset of these special Ruby-specific methods. We want to utilize that
|
@@ -46,7 +46,9 @@ module RubyLsp
|
|
46
46
|
diagnostics.concat(syntax_error_diagnostics, syntax_warning_diagnostics)
|
47
47
|
|
48
48
|
# Running RuboCop is slow, so to avoid excessive runs we only do so if the file is syntactically valid
|
49
|
-
|
49
|
+
if @document.syntax_error? || @active_linters.empty? || @document.past_expensive_limit?
|
50
|
+
return diagnostics
|
51
|
+
end
|
50
52
|
|
51
53
|
@active_linters.each do |linter|
|
52
54
|
linter_diagnostics = linter.run_diagnostic(@uri, @document)
|
@@ -19,6 +19,16 @@ module RubyLsp
|
|
19
19
|
# some_invocation # --> semantic highlighting: method invocation
|
20
20
|
# var # --> semantic highlighting: local variable
|
21
21
|
# end
|
22
|
+
#
|
23
|
+
# # Strategy
|
24
|
+
#
|
25
|
+
# To maximize editor performance, the Ruby LSP will return the minimum number of semantic tokens, since applying
|
26
|
+
# them is an expensive operation for the client. This means that the server only returns tokens for ambiguous pieces
|
27
|
+
# of syntax, such as method invocations with no receivers or parenthesis (which can be confused with local
|
28
|
+
# variables).
|
29
|
+
#
|
30
|
+
# Offloading as much handling as possible to Text Mate grammars or equivalent will guarantee responsiveness in the
|
31
|
+
# editor and allow for a much smoother experience.
|
22
32
|
# ```
|
23
33
|
class SemanticHighlighting < Request
|
24
34
|
extend T::Sig
|
@@ -35,28 +45,125 @@ module RubyLsp
|
|
35
45
|
token_modifiers: ResponseBuilders::SemanticHighlighting::TOKEN_MODIFIERS.keys,
|
36
46
|
),
|
37
47
|
range: true,
|
38
|
-
full: { delta:
|
48
|
+
full: { delta: true },
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
# The compute_delta method receives the current semantic tokens and the previous semantic tokens and then tries
|
53
|
+
# to compute the smallest possible semantic token edit that will turn previous into current
|
54
|
+
sig do
|
55
|
+
params(
|
56
|
+
current_tokens: T::Array[Integer],
|
57
|
+
previous_tokens: T::Array[Integer],
|
58
|
+
result_id: String,
|
59
|
+
).returns(Interface::SemanticTokensDelta)
|
60
|
+
end
|
61
|
+
def compute_delta(current_tokens, previous_tokens, result_id)
|
62
|
+
# Find the index of the first token that is different between the two sets of tokens
|
63
|
+
first_different_position = current_tokens.zip(previous_tokens).find_index { |new, old| new != old }
|
64
|
+
|
65
|
+
# When deleting a token from the end, the first_different_position will be nil, but since we're removing at
|
66
|
+
# the end, then we have to initialize it to the length of the current tokens after the deletion
|
67
|
+
if !first_different_position && current_tokens.length < previous_tokens.length
|
68
|
+
first_different_position = current_tokens.length
|
69
|
+
end
|
70
|
+
|
71
|
+
unless first_different_position
|
72
|
+
return Interface::SemanticTokensDelta.new(result_id: result_id, edits: [])
|
73
|
+
end
|
74
|
+
|
75
|
+
# Filter the tokens based on the first different position. This must happen at this stage, before we try to
|
76
|
+
# find the next position from the end or else we risk confusing sets of token that may have different lengths,
|
77
|
+
# but end with the exact same token
|
78
|
+
old_tokens = T.must(previous_tokens[first_different_position...])
|
79
|
+
new_tokens = T.must(current_tokens[first_different_position...])
|
80
|
+
|
81
|
+
# Then search from the end to find the first token that doesn't match. Since the user is normally editing the
|
82
|
+
# middle of the file, this will minimize the number of edits since the end of the token array has not changed
|
83
|
+
first_different_token_from_end = new_tokens.reverse.zip(old_tokens.reverse).find_index do |new, old|
|
84
|
+
new != old
|
85
|
+
end || 0
|
86
|
+
|
87
|
+
# Filter the old and new tokens to only the section that will be replaced/inserted/deleted
|
88
|
+
old_tokens = T.must(old_tokens[...old_tokens.length - first_different_token_from_end])
|
89
|
+
new_tokens = T.must(new_tokens[...new_tokens.length - first_different_token_from_end])
|
90
|
+
|
91
|
+
# And we send back a single edit, replacing an entire section for the new tokens
|
92
|
+
Interface::SemanticTokensDelta.new(
|
93
|
+
result_id: result_id,
|
94
|
+
edits: [{ start: first_different_position, deleteCount: old_tokens.length, data: new_tokens }],
|
39
95
|
)
|
40
96
|
end
|
97
|
+
|
98
|
+
sig { returns(Integer) }
|
99
|
+
def next_result_id
|
100
|
+
@mutex.synchronize do
|
101
|
+
@result_id += 1
|
102
|
+
end
|
103
|
+
end
|
41
104
|
end
|
42
105
|
|
43
|
-
|
44
|
-
|
106
|
+
@result_id = T.let(0, Integer)
|
107
|
+
@mutex = T.let(Mutex.new, Mutex)
|
108
|
+
|
109
|
+
sig do
|
110
|
+
params(
|
111
|
+
global_state: GlobalState,
|
112
|
+
dispatcher: Prism::Dispatcher,
|
113
|
+
document: T.any(RubyDocument, ERBDocument),
|
114
|
+
previous_result_id: T.nilable(String),
|
115
|
+
range: T.nilable(T::Range[Integer]),
|
116
|
+
).void
|
117
|
+
end
|
118
|
+
def initialize(global_state, dispatcher, document, previous_result_id, range: nil)
|
45
119
|
super()
|
120
|
+
|
121
|
+
@document = document
|
122
|
+
@previous_result_id = previous_result_id
|
123
|
+
@range = range
|
124
|
+
@result_id = T.let(SemanticHighlighting.next_result_id.to_s, String)
|
46
125
|
@response_builder = T.let(
|
47
126
|
ResponseBuilders::SemanticHighlighting.new(global_state.encoding),
|
48
127
|
ResponseBuilders::SemanticHighlighting,
|
49
128
|
)
|
50
|
-
Listeners::SemanticHighlighting.new(dispatcher, @response_builder
|
129
|
+
Listeners::SemanticHighlighting.new(dispatcher, @response_builder)
|
51
130
|
|
52
131
|
Addon.addons.each do |addon|
|
53
132
|
addon.create_semantic_highlighting_listener(@response_builder, dispatcher)
|
54
133
|
end
|
55
134
|
end
|
56
135
|
|
57
|
-
sig { override.returns(Interface::SemanticTokens) }
|
136
|
+
sig { override.returns(T.any(Interface::SemanticTokens, Interface::SemanticTokensDelta)) }
|
58
137
|
def perform
|
59
|
-
@
|
138
|
+
previous_tokens = @document.semantic_tokens
|
139
|
+
tokens = @response_builder.response
|
140
|
+
encoded_tokens = ResponseBuilders::SemanticHighlighting::SemanticTokenEncoder.new.encode(tokens)
|
141
|
+
full_response = Interface::SemanticTokens.new(result_id: @result_id, data: encoded_tokens)
|
142
|
+
@document.semantic_tokens = full_response
|
143
|
+
|
144
|
+
if @range
|
145
|
+
tokens_within_range = tokens.select { |token| @range.cover?(token.start_line - 1) }
|
146
|
+
|
147
|
+
return Interface::SemanticTokens.new(
|
148
|
+
result_id: @result_id,
|
149
|
+
data: ResponseBuilders::SemanticHighlighting::SemanticTokenEncoder.new.encode(tokens_within_range),
|
150
|
+
)
|
151
|
+
end
|
152
|
+
|
153
|
+
# Semantic tokens full delta
|
154
|
+
if @previous_result_id
|
155
|
+
response = if previous_tokens.is_a?(Interface::SemanticTokens) &&
|
156
|
+
previous_tokens.result_id == @previous_result_id
|
157
|
+
Requests::SemanticHighlighting.compute_delta(encoded_tokens, previous_tokens.data, @result_id)
|
158
|
+
else
|
159
|
+
full_response
|
160
|
+
end
|
161
|
+
|
162
|
+
return response
|
163
|
+
end
|
164
|
+
|
165
|
+
# Semantic tokens full
|
166
|
+
full_response
|
60
167
|
end
|
61
168
|
end
|
62
169
|
end
|
@@ -37,15 +37,6 @@ module RubyLsp
|
|
37
37
|
)
|
38
38
|
end
|
39
39
|
|
40
|
-
sig { params(node: T.nilable(Prism::Node), range: T.nilable(T::Range[Integer])).returns(T::Boolean) }
|
41
|
-
def visible?(node, range)
|
42
|
-
return true if range.nil?
|
43
|
-
return false if node.nil?
|
44
|
-
|
45
|
-
loc = node.location
|
46
|
-
range.cover?(loc.start_line - 1) && range.cover?(loc.end_line - 1)
|
47
|
-
end
|
48
|
-
|
49
40
|
sig do
|
50
41
|
params(
|
51
42
|
node: Prism::Node,
|
@@ -91,9 +91,9 @@ module RubyLsp
|
|
91
91
|
@stack.last
|
92
92
|
end
|
93
93
|
|
94
|
-
sig { override.returns(
|
94
|
+
sig { override.returns(T::Array[SemanticToken]) }
|
95
95
|
def response
|
96
|
-
|
96
|
+
@stack
|
97
97
|
end
|
98
98
|
|
99
99
|
class SemanticToken
|
@@ -162,7 +162,7 @@ module RubyLsp
|
|
162
162
|
sig do
|
163
163
|
params(
|
164
164
|
tokens: T::Array[SemanticToken],
|
165
|
-
).returns(
|
165
|
+
).returns(T::Array[Integer])
|
166
166
|
end
|
167
167
|
def encode(tokens)
|
168
168
|
sorted_tokens = tokens.sort_by.with_index do |token, index|
|
@@ -176,7 +176,7 @@ module RubyLsp
|
|
176
176
|
compute_delta(token)
|
177
177
|
end
|
178
178
|
|
179
|
-
|
179
|
+
delta
|
180
180
|
end
|
181
181
|
|
182
182
|
# The delta array is computed according to the LSP specification:
|
data/lib/ruby_lsp/server.rb
CHANGED
@@ -23,6 +23,7 @@ module RubyLsp
|
|
23
23
|
run_initialize(message)
|
24
24
|
when "initialized"
|
25
25
|
send_log_message("Finished initializing Ruby LSP!") unless @test_mode
|
26
|
+
|
26
27
|
run_initialized
|
27
28
|
when "textDocument/didOpen"
|
28
29
|
text_document_did_open(message)
|
@@ -40,6 +41,8 @@ module RubyLsp
|
|
40
41
|
text_document_code_lens(message)
|
41
42
|
when "textDocument/semanticTokens/full"
|
42
43
|
text_document_semantic_tokens_full(message)
|
44
|
+
when "textDocument/semanticTokens/full/delta"
|
45
|
+
text_document_semantic_tokens_delta(message)
|
43
46
|
when "textDocument/foldingRange"
|
44
47
|
text_document_folding_range(message)
|
45
48
|
when "textDocument/semanticTokens/range"
|
@@ -304,13 +307,26 @@ module RubyLsp
|
|
304
307
|
Document::LanguageId::Ruby
|
305
308
|
end
|
306
309
|
|
307
|
-
@store.set(
|
310
|
+
document = @store.set(
|
308
311
|
uri: text_document[:uri],
|
309
312
|
source: text_document[:text],
|
310
313
|
version: text_document[:version],
|
311
314
|
encoding: @global_state.encoding,
|
312
315
|
language_id: language_id,
|
313
316
|
)
|
317
|
+
|
318
|
+
if document.past_expensive_limit?
|
319
|
+
send_message(
|
320
|
+
Notification.new(
|
321
|
+
method: "window/showMessage",
|
322
|
+
params: Interface::ShowMessageParams.new(
|
323
|
+
type: Constant::MessageType::WARNING,
|
324
|
+
message: "This file is too long. For performance reasons, semantic highlighting and " \
|
325
|
+
"diagnostics will be disabled",
|
326
|
+
),
|
327
|
+
),
|
328
|
+
)
|
329
|
+
end
|
314
330
|
end
|
315
331
|
end
|
316
332
|
|
@@ -378,7 +394,7 @@ module RubyLsp
|
|
378
394
|
|
379
395
|
# If the response has already been cached by another request, return it
|
380
396
|
cached_response = document.cache_get(message[:method])
|
381
|
-
if cached_response
|
397
|
+
if cached_response != Document::EMPTY_CACHE
|
382
398
|
send_message(Result.new(id: message[:id], response: cached_response))
|
383
399
|
return
|
384
400
|
end
|
@@ -391,8 +407,6 @@ module RubyLsp
|
|
391
407
|
document_symbol = Requests::DocumentSymbol.new(uri, dispatcher)
|
392
408
|
document_link = Requests::DocumentLink.new(uri, parse_result.comments, dispatcher)
|
393
409
|
code_lens = Requests::CodeLens.new(@global_state, uri, dispatcher)
|
394
|
-
|
395
|
-
semantic_highlighting = Requests::SemanticHighlighting.new(@global_state, dispatcher)
|
396
410
|
dispatcher.dispatch(parse_result.value)
|
397
411
|
|
398
412
|
# Store all responses retrieve in this round of visits in the cache and then return the response for the request
|
@@ -401,19 +415,61 @@ module RubyLsp
|
|
401
415
|
document.cache_set("textDocument/documentSymbol", document_symbol.perform)
|
402
416
|
document.cache_set("textDocument/documentLink", document_link.perform)
|
403
417
|
document.cache_set("textDocument/codeLens", code_lens.perform)
|
404
|
-
|
405
|
-
"textDocument/semanticTokens/full",
|
406
|
-
semantic_highlighting.perform,
|
407
|
-
)
|
418
|
+
|
408
419
|
send_message(Result.new(id: message[:id], response: document.cache_get(message[:method])))
|
409
420
|
end
|
410
421
|
|
411
422
|
alias_method :text_document_document_symbol, :run_combined_requests
|
412
423
|
alias_method :text_document_document_link, :run_combined_requests
|
413
424
|
alias_method :text_document_code_lens, :run_combined_requests
|
414
|
-
alias_method :text_document_semantic_tokens_full, :run_combined_requests
|
415
425
|
alias_method :text_document_folding_range, :run_combined_requests
|
416
426
|
|
427
|
+
sig { params(message: T::Hash[Symbol, T.untyped]).void }
|
428
|
+
def text_document_semantic_tokens_full(message)
|
429
|
+
document = @store.get(message.dig(:params, :textDocument, :uri))
|
430
|
+
|
431
|
+
if document.past_expensive_limit?
|
432
|
+
send_empty_response(message[:id])
|
433
|
+
return
|
434
|
+
end
|
435
|
+
|
436
|
+
unless document.is_a?(RubyDocument) || document.is_a?(ERBDocument)
|
437
|
+
send_empty_response(message[:id])
|
438
|
+
return
|
439
|
+
end
|
440
|
+
|
441
|
+
dispatcher = Prism::Dispatcher.new
|
442
|
+
semantic_highlighting = Requests::SemanticHighlighting.new(@global_state, dispatcher, document, nil)
|
443
|
+
dispatcher.visit(document.parse_result.value)
|
444
|
+
|
445
|
+
send_message(Result.new(id: message[:id], response: semantic_highlighting.perform))
|
446
|
+
end
|
447
|
+
|
448
|
+
sig { params(message: T::Hash[Symbol, T.untyped]).void }
|
449
|
+
def text_document_semantic_tokens_delta(message)
|
450
|
+
document = @store.get(message.dig(:params, :textDocument, :uri))
|
451
|
+
|
452
|
+
if document.past_expensive_limit?
|
453
|
+
send_empty_response(message[:id])
|
454
|
+
return
|
455
|
+
end
|
456
|
+
|
457
|
+
unless document.is_a?(RubyDocument) || document.is_a?(ERBDocument)
|
458
|
+
send_empty_response(message[:id])
|
459
|
+
return
|
460
|
+
end
|
461
|
+
|
462
|
+
dispatcher = Prism::Dispatcher.new
|
463
|
+
request = Requests::SemanticHighlighting.new(
|
464
|
+
@global_state,
|
465
|
+
dispatcher,
|
466
|
+
document,
|
467
|
+
message.dig(:params, :previousResultId),
|
468
|
+
)
|
469
|
+
dispatcher.visit(document.parse_result.value)
|
470
|
+
send_message(Result.new(id: message[:id], response: request.perform))
|
471
|
+
end
|
472
|
+
|
417
473
|
sig { params(message: T::Hash[Symbol, T.untyped]).void }
|
418
474
|
def text_document_semantic_tokens_range(message)
|
419
475
|
params = message[:params]
|
@@ -421,20 +477,26 @@ module RubyLsp
|
|
421
477
|
uri = params.dig(:textDocument, :uri)
|
422
478
|
document = @store.get(uri)
|
423
479
|
|
424
|
-
|
480
|
+
if document.past_expensive_limit?
|
425
481
|
send_empty_response(message[:id])
|
426
482
|
return
|
427
483
|
end
|
428
484
|
|
429
|
-
|
430
|
-
|
485
|
+
unless document.is_a?(RubyDocument) || document.is_a?(ERBDocument)
|
486
|
+
send_empty_response(message[:id])
|
487
|
+
return
|
488
|
+
end
|
431
489
|
|
432
490
|
dispatcher = Prism::Dispatcher.new
|
433
|
-
request = Requests::SemanticHighlighting.new(
|
491
|
+
request = Requests::SemanticHighlighting.new(
|
492
|
+
@global_state,
|
493
|
+
dispatcher,
|
494
|
+
document,
|
495
|
+
nil,
|
496
|
+
range: range.dig(:start, :line)..range.dig(:end, :line),
|
497
|
+
)
|
434
498
|
dispatcher.visit(document.parse_result.value)
|
435
|
-
|
436
|
-
response = request.perform
|
437
|
-
send_message(Result.new(id: message[:id], response: response))
|
499
|
+
send_message(Result.new(id: message[:id], response: request.perform))
|
438
500
|
end
|
439
501
|
|
440
502
|
sig { params(message: T::Hash[Symbol, T.untyped]).void }
|
@@ -879,7 +941,7 @@ module RubyLsp
|
|
879
941
|
# Indexing produces a high number of short lived object allocations. That might lead to some fragmentation and
|
880
942
|
# an unnecessarily expanded heap. Compacting ensures that the heap is as small as possible and that future
|
881
943
|
# allocations and garbage collections are faster
|
882
|
-
GC.compact
|
944
|
+
GC.compact unless @test_mode
|
883
945
|
|
884
946
|
# Always end the progress notification even if indexing failed or else it never goes away and the user has no
|
885
947
|
# way of dismissing it
|
@@ -1000,10 +1062,10 @@ module RubyLsp
|
|
1000
1062
|
|
1001
1063
|
return unless indexing_options
|
1002
1064
|
|
1065
|
+
configuration = @global_state.index.configuration
|
1066
|
+
configuration.workspace_path = @global_state.workspace_path
|
1003
1067
|
# The index expects snake case configurations, but VS Code standardizes on camel case settings
|
1004
|
-
|
1005
|
-
indexing_options.transform_keys { |key| key.to_s.gsub(/([A-Z])/, "_\\1").downcase },
|
1006
|
-
)
|
1068
|
+
configuration.apply_config(indexing_options.transform_keys { |key| key.to_s.gsub(/([A-Z])/, "_\\1").downcase })
|
1007
1069
|
end
|
1008
1070
|
end
|
1009
1071
|
end
|
@@ -43,7 +43,7 @@ module RubyLsp
|
|
43
43
|
@gemfile_name = T.let(@gemfile&.basename&.to_s || "Gemfile", String)
|
44
44
|
|
45
45
|
# Custom bundle paths
|
46
|
-
@custom_dir = T.let(Pathname.new(".ruby-lsp").expand_path(
|
46
|
+
@custom_dir = T.let(Pathname.new(".ruby-lsp").expand_path(@project_path), Pathname)
|
47
47
|
@custom_gemfile = T.let(@custom_dir + @gemfile_name, Pathname)
|
48
48
|
@custom_lockfile = T.let(@custom_dir + (@lockfile&.basename || "Gemfile.lock"), Pathname)
|
49
49
|
@lockfile_hash_path = T.let(@custom_dir + "main_lockfile_hash", Pathname)
|
@@ -183,14 +183,14 @@ module RubyLsp
|
|
183
183
|
# `.ruby-lsp` folder, which is not the user's intention. For example, if the path is configured as `vendor`, we
|
184
184
|
# want to install it in the top level `vendor` and not `.ruby-lsp/vendor`
|
185
185
|
path = Bundler.settings["path"]
|
186
|
-
expanded_path = File.expand_path(path,
|
186
|
+
expanded_path = File.expand_path(path, @project_path) if path
|
187
187
|
|
188
188
|
# Use the absolute `BUNDLE_PATH` to prevent accidentally creating unwanted folders under `.ruby-lsp`
|
189
189
|
env = {}
|
190
190
|
env["BUNDLE_GEMFILE"] = bundle_gemfile.to_s
|
191
191
|
env["BUNDLE_PATH"] = expanded_path if expanded_path
|
192
192
|
|
193
|
-
local_config_path = File.join(
|
193
|
+
local_config_path = File.join(@project_path, ".bundle")
|
194
194
|
env["BUNDLE_APP_CONFIG"] = local_config_path if Dir.exist?(local_config_path)
|
195
195
|
|
196
196
|
# If `ruby-lsp` and `debug` (and potentially `ruby-lsp-rails`) are already in the Gemfile, then we shouldn't try
|
@@ -286,7 +286,7 @@ module RubyLsp
|
|
286
286
|
sig { returns(T::Boolean) }
|
287
287
|
def rails_app?
|
288
288
|
config = Pathname.new("config/application.rb").expand_path
|
289
|
-
application_contents = config.read if config.exist?
|
289
|
+
application_contents = config.read(external_encoding: Encoding::UTF_8) if config.exist?
|
290
290
|
return false unless application_contents
|
291
291
|
|
292
292
|
/class .* < (::)?Rails::Application/.match?(application_contents)
|
data/lib/ruby_lsp/store.rb
CHANGED
@@ -66,7 +66,7 @@ module RubyLsp
|
|
66
66
|
version: Integer,
|
67
67
|
language_id: Document::LanguageId,
|
68
68
|
encoding: Encoding,
|
69
|
-
).
|
69
|
+
).returns(Document[T.untyped])
|
70
70
|
end
|
71
71
|
def set(uri:, source:, version:, language_id:, encoding: Encoding::UTF_8)
|
72
72
|
@state[uri.to_s] = case language_id
|
data/lib/ruby_lsp/test_helper.rb
CHANGED
@@ -20,8 +20,8 @@ module RubyLsp
|
|
20
20
|
def with_server(source = nil, uri = Kernel.URI("file:///fake.rb"), stub_no_typechecker: false, load_addons: true,
|
21
21
|
&block)
|
22
22
|
server = RubyLsp::Server.new(test_mode: true)
|
23
|
-
server.global_state.stubs(:has_type_checker).returns(false) if stub_no_typechecker
|
24
23
|
server.global_state.apply_options({ initializationOptions: { experimentalFeaturesEnabled: true } })
|
24
|
+
server.global_state.instance_variable_set(:@has_type_checker, false) if stub_no_typechecker
|
25
25
|
language_id = uri.to_s.end_with?(".erb") ? "erb" : "ruby"
|
26
26
|
|
27
27
|
if source
|
@@ -36,9 +36,47 @@ module RubyLsp
|
|
36
36
|
def infer_receiver_for_call_node(node, node_context)
|
37
37
|
receiver = node.receiver
|
38
38
|
|
39
|
+
# For receivers inside parenthesis, such as ranges like (0...2), we need to unwrap the parenthesis to get the
|
40
|
+
# actual node
|
41
|
+
if receiver.is_a?(Prism::ParenthesesNode)
|
42
|
+
statements = receiver.body
|
43
|
+
|
44
|
+
if statements.is_a?(Prism::StatementsNode)
|
45
|
+
body = statements.body
|
46
|
+
|
47
|
+
if body.length == 1
|
48
|
+
receiver = body.first
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
39
53
|
case receiver
|
40
54
|
when Prism::SelfNode, nil
|
41
55
|
self_receiver_handling(node_context)
|
56
|
+
when Prism::StringNode
|
57
|
+
Type.new("String")
|
58
|
+
when Prism::SymbolNode
|
59
|
+
Type.new("Symbol")
|
60
|
+
when Prism::ArrayNode
|
61
|
+
Type.new("Array")
|
62
|
+
when Prism::HashNode
|
63
|
+
Type.new("Hash")
|
64
|
+
when Prism::IntegerNode
|
65
|
+
Type.new("Integer")
|
66
|
+
when Prism::FloatNode
|
67
|
+
Type.new("Float")
|
68
|
+
when Prism::RegularExpressionNode
|
69
|
+
Type.new("Regexp")
|
70
|
+
when Prism::NilNode
|
71
|
+
Type.new("NilClass")
|
72
|
+
when Prism::TrueNode
|
73
|
+
Type.new("TrueClass")
|
74
|
+
when Prism::FalseNode
|
75
|
+
Type.new("FalseClass")
|
76
|
+
when Prism::RangeNode
|
77
|
+
Type.new("Range")
|
78
|
+
when Prism::LambdaNode
|
79
|
+
Type.new("Proc")
|
42
80
|
when Prism::ConstantPathNode, Prism::ConstantReadNode
|
43
81
|
# When the receiver is a constant reference, we have to try to resolve it to figure out the right
|
44
82
|
# receiver. But since the invocation is directly on the constant, that's the singleton context of that
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby-lsp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.17.
|
4
|
+
version: 0.17.16
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shopify
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-08-
|
11
|
+
date: 2024-08-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: language_server-protocol
|