ruby-lsp 0.17.13 → 0.17.15
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/exe/ruby-lsp +2 -0
- 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 +9 -114
- data/lib/ruby_lsp/erb_document.rb +16 -2
- data/lib/ruby_lsp/global_state.rb +1 -1
- data/lib/ruby_lsp/internal.rb +1 -0
- data/lib/ruby_lsp/listeners/definition.rb +8 -5
- data/lib/ruby_lsp/rbs_document.rb +41 -0
- data/lib/ruby_lsp/requests/code_action_resolve.rb +34 -18
- data/lib/ruby_lsp/requests/code_actions.rb +1 -1
- data/lib/ruby_lsp/requests/completion.rb +2 -2
- data/lib/ruby_lsp/requests/definition.rb +1 -1
- data/lib/ruby_lsp/requests/diagnostics.rb +1 -1
- data/lib/ruby_lsp/requests/document_highlight.rb +1 -1
- data/lib/ruby_lsp/requests/formatting.rb +1 -1
- data/lib/ruby_lsp/requests/hover.rb +1 -1
- data/lib/ruby_lsp/requests/inlay_hints.rb +1 -1
- data/lib/ruby_lsp/requests/on_type_formatting.rb +1 -1
- data/lib/ruby_lsp/requests/prepare_type_hierarchy.rb +1 -1
- data/lib/ruby_lsp/requests/selection_ranges.rb +2 -1
- data/lib/ruby_lsp/requests/show_syntax_tree.rb +1 -1
- data/lib/ruby_lsp/requests/signature_help.rb +1 -1
- data/lib/ruby_lsp/requests/support/formatter.rb +2 -2
- data/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb +1 -1
- data/lib/ruby_lsp/requests/support/rubocop_formatter.rb +2 -2
- data/lib/ruby_lsp/requests/support/syntax_tree_formatter.rb +2 -2
- data/lib/ruby_lsp/ruby_document.rb +119 -1
- data/lib/ruby_lsp/server.rb +96 -8
- data/lib/ruby_lsp/setup_bundler.rb +18 -7
- data/lib/ruby_lsp/store.rb +10 -6
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 43d715172510bba67801670dbfc574eed9851b9852aaea3f6e210acd74734af4
|
4
|
+
data.tar.gz: b35b3a55c3d65987390e4b125a090ff8f6da902bfa7ab9c08eb9eb8c72d5157f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ef8a2ca24755eef8c5ed1146f1f6c40ec725988e9dd6a51efb326583e5606a221f733625f08d4dad5bb6091675c39fb18a485ff8c0bed1dc0b2e221a4390c585
|
7
|
+
data.tar.gz: 16101462585105a9a201f6341908f0b238f77f4acaa6dac723c95e35fcddc7c7b68d937bead5beecc14ae6bcac276b2f813523304f7eaded3132d3aa07eb943b
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.17.
|
1
|
+
0.17.15
|
data/exe/ruby-lsp
CHANGED
@@ -81,6 +81,8 @@ require "ruby_lsp/load_sorbet"
|
|
81
81
|
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
82
82
|
require "ruby_lsp/internal"
|
83
83
|
|
84
|
+
T::Utils.run_all_sig_blocks
|
85
|
+
|
84
86
|
if options[:debug]
|
85
87
|
if ["x64-mingw-ucrt", "x64-mingw32"].include?(RUBY_PLATFORM)
|
86
88
|
$stderr.puts "Debugging is not supported on Windows"
|
@@ -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
@@ -7,15 +7,19 @@ module RubyLsp
|
|
7
7
|
enums do
|
8
8
|
Ruby = new("ruby")
|
9
9
|
ERB = new("erb")
|
10
|
+
RBS = new("rbs")
|
10
11
|
end
|
11
12
|
end
|
12
13
|
|
13
14
|
extend T::Sig
|
14
15
|
extend T::Helpers
|
16
|
+
extend T::Generic
|
17
|
+
|
18
|
+
ParseResultType = type_member
|
15
19
|
|
16
20
|
abstract!
|
17
21
|
|
18
|
-
sig { returns(
|
22
|
+
sig { returns(ParseResultType) }
|
19
23
|
attr_reader :parse_result
|
20
24
|
|
21
25
|
sig { returns(String) }
|
@@ -38,10 +42,10 @@ module RubyLsp
|
|
38
42
|
@version = T.let(version, Integer)
|
39
43
|
@uri = T.let(uri, URI::Generic)
|
40
44
|
@needs_parsing = T.let(true, T::Boolean)
|
41
|
-
@parse_result = T.let(parse,
|
45
|
+
@parse_result = T.let(parse, ParseResultType)
|
42
46
|
end
|
43
47
|
|
44
|
-
sig { params(other: Document).returns(T::Boolean) }
|
48
|
+
sig { params(other: Document[T.untyped]).returns(T::Boolean) }
|
45
49
|
def ==(other)
|
46
50
|
self.class == other.class && uri == other.uri && @source == other.source
|
47
51
|
end
|
@@ -54,7 +58,7 @@ module RubyLsp
|
|
54
58
|
type_parameters(:T)
|
55
59
|
.params(
|
56
60
|
request_name: String,
|
57
|
-
block: T.proc.params(document: Document).returns(T.type_parameter(:T)),
|
61
|
+
block: T.proc.params(document: Document[ParseResultType]).returns(T.type_parameter(:T)),
|
58
62
|
).returns(T.type_parameter(:T))
|
59
63
|
end
|
60
64
|
def cache_fetch(request_name, &block)
|
@@ -93,7 +97,7 @@ module RubyLsp
|
|
93
97
|
@cache.clear
|
94
98
|
end
|
95
99
|
|
96
|
-
sig { abstract.returns(
|
100
|
+
sig { abstract.returns(ParseResultType) }
|
97
101
|
def parse; end
|
98
102
|
|
99
103
|
sig { abstract.returns(T::Boolean) }
|
@@ -104,115 +108,6 @@ module RubyLsp
|
|
104
108
|
Scanner.new(@source, @encoding)
|
105
109
|
end
|
106
110
|
|
107
|
-
sig do
|
108
|
-
params(
|
109
|
-
position: T::Hash[Symbol, T.untyped],
|
110
|
-
node_types: T::Array[T.class_of(Prism::Node)],
|
111
|
-
).returns(NodeContext)
|
112
|
-
end
|
113
|
-
def locate_node(position, node_types: [])
|
114
|
-
locate(@parse_result.value, create_scanner.find_char_position(position), node_types: node_types)
|
115
|
-
end
|
116
|
-
|
117
|
-
sig do
|
118
|
-
params(
|
119
|
-
node: Prism::Node,
|
120
|
-
char_position: Integer,
|
121
|
-
node_types: T::Array[T.class_of(Prism::Node)],
|
122
|
-
).returns(NodeContext)
|
123
|
-
end
|
124
|
-
def locate(node, char_position, node_types: [])
|
125
|
-
queue = T.let(node.child_nodes.compact, T::Array[T.nilable(Prism::Node)])
|
126
|
-
closest = node
|
127
|
-
parent = T.let(nil, T.nilable(Prism::Node))
|
128
|
-
nesting_nodes = T.let(
|
129
|
-
[],
|
130
|
-
T::Array[T.any(
|
131
|
-
Prism::ClassNode,
|
132
|
-
Prism::ModuleNode,
|
133
|
-
Prism::SingletonClassNode,
|
134
|
-
Prism::DefNode,
|
135
|
-
Prism::BlockNode,
|
136
|
-
Prism::LambdaNode,
|
137
|
-
Prism::ProgramNode,
|
138
|
-
)],
|
139
|
-
)
|
140
|
-
|
141
|
-
nesting_nodes << node if node.is_a?(Prism::ProgramNode)
|
142
|
-
call_node = T.let(nil, T.nilable(Prism::CallNode))
|
143
|
-
|
144
|
-
until queue.empty?
|
145
|
-
candidate = queue.shift
|
146
|
-
|
147
|
-
# Skip nil child nodes
|
148
|
-
next if candidate.nil?
|
149
|
-
|
150
|
-
# Add the next child_nodes to the queue to be processed. The order here is important! We want to move in the
|
151
|
-
# same order as the visiting mechanism, which means searching the child nodes before moving on to the next
|
152
|
-
# sibling
|
153
|
-
T.unsafe(queue).unshift(*candidate.child_nodes)
|
154
|
-
|
155
|
-
# Skip if the current node doesn't cover the desired position
|
156
|
-
loc = candidate.location
|
157
|
-
next unless (loc.start_offset...loc.end_offset).cover?(char_position)
|
158
|
-
|
159
|
-
# If the node's start character is already past the position, then we should've found the closest node
|
160
|
-
# already
|
161
|
-
break if char_position < loc.start_offset
|
162
|
-
|
163
|
-
# If the candidate starts after the end of the previous nesting level, then we've exited that nesting level and
|
164
|
-
# need to pop the stack
|
165
|
-
previous_level = nesting_nodes.last
|
166
|
-
nesting_nodes.pop if previous_level && loc.start_offset > previous_level.location.end_offset
|
167
|
-
|
168
|
-
# Keep track of the nesting where we found the target. This is used to determine the fully qualified name of the
|
169
|
-
# target when it is a constant
|
170
|
-
case candidate
|
171
|
-
when Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode, Prism::DefNode, Prism::BlockNode,
|
172
|
-
Prism::LambdaNode
|
173
|
-
nesting_nodes << candidate
|
174
|
-
end
|
175
|
-
|
176
|
-
if candidate.is_a?(Prism::CallNode)
|
177
|
-
arg_loc = candidate.arguments&.location
|
178
|
-
blk_loc = candidate.block&.location
|
179
|
-
if (arg_loc && (arg_loc.start_offset...arg_loc.end_offset).cover?(char_position)) ||
|
180
|
-
(blk_loc && (blk_loc.start_offset...blk_loc.end_offset).cover?(char_position))
|
181
|
-
call_node = candidate
|
182
|
-
end
|
183
|
-
end
|
184
|
-
|
185
|
-
# If there are node types to filter by, and the current node is not one of those types, then skip it
|
186
|
-
next if node_types.any? && node_types.none? { |type| candidate.class == type }
|
187
|
-
|
188
|
-
# If the current node is narrower than or equal to the previous closest node, then it is more precise
|
189
|
-
closest_loc = closest.location
|
190
|
-
if loc.end_offset - loc.start_offset <= closest_loc.end_offset - closest_loc.start_offset
|
191
|
-
parent = closest
|
192
|
-
closest = candidate
|
193
|
-
end
|
194
|
-
end
|
195
|
-
|
196
|
-
# When targeting the constant part of a class/module definition, we do not want the nesting to be duplicated. That
|
197
|
-
# is, when targeting Bar in the following example:
|
198
|
-
#
|
199
|
-
# ```ruby
|
200
|
-
# class Foo::Bar; end
|
201
|
-
# ```
|
202
|
-
# The correct target is `Foo::Bar` with an empty nesting. `Foo::Bar` should not appear in the nesting stack, even
|
203
|
-
# though the class/module node does indeed enclose the target, because it would lead to incorrect behavior
|
204
|
-
if closest.is_a?(Prism::ConstantReadNode) || closest.is_a?(Prism::ConstantPathNode)
|
205
|
-
last_level = nesting_nodes.last
|
206
|
-
|
207
|
-
if (last_level.is_a?(Prism::ModuleNode) || last_level.is_a?(Prism::ClassNode)) &&
|
208
|
-
last_level.constant_path == closest
|
209
|
-
nesting_nodes.pop
|
210
|
-
end
|
211
|
-
end
|
212
|
-
|
213
|
-
NodeContext.new(closest, parent, nesting_nodes, call_node)
|
214
|
-
end
|
215
|
-
|
216
111
|
class Scanner
|
217
112
|
extend T::Sig
|
218
113
|
|
@@ -4,15 +4,19 @@
|
|
4
4
|
module RubyLsp
|
5
5
|
class ERBDocument < Document
|
6
6
|
extend T::Sig
|
7
|
+
extend T::Generic
|
7
8
|
|
8
|
-
|
9
|
+
ParseResultType = type_member { { fixed: Prism::ParseResult } }
|
10
|
+
|
11
|
+
sig { override.returns(ParseResultType) }
|
9
12
|
def parse
|
10
13
|
return @parse_result unless @needs_parsing
|
11
14
|
|
12
15
|
@needs_parsing = false
|
13
16
|
scanner = ERBScanner.new(@source)
|
14
17
|
scanner.scan
|
15
|
-
|
18
|
+
# assigning empty scopes to turn Prism into eval mode
|
19
|
+
@parse_result = Prism.parse(scanner.ruby, scopes: [[]])
|
16
20
|
end
|
17
21
|
|
18
22
|
sig { override.returns(T::Boolean) }
|
@@ -25,6 +29,16 @@ module RubyLsp
|
|
25
29
|
LanguageId::ERB
|
26
30
|
end
|
27
31
|
|
32
|
+
sig do
|
33
|
+
params(
|
34
|
+
position: T::Hash[Symbol, T.untyped],
|
35
|
+
node_types: T::Array[T.class_of(Prism::Node)],
|
36
|
+
).returns(NodeContext)
|
37
|
+
end
|
38
|
+
def locate_node(position, node_types: [])
|
39
|
+
RubyDocument.locate(@parse_result.value, create_scanner.find_char_position(position), node_types: node_types)
|
40
|
+
end
|
41
|
+
|
28
42
|
class ERBScanner
|
29
43
|
extend T::Sig
|
30
44
|
|
@@ -93,7 +93,7 @@ module RubyLsp
|
|
93
93
|
@test_library = detect_test_library(direct_dependencies)
|
94
94
|
notifications << Notification.window_log_message("Detected test library: #{@test_library}")
|
95
95
|
|
96
|
-
@has_type_checker = detect_typechecker(
|
96
|
+
@has_type_checker = detect_typechecker(all_dependencies)
|
97
97
|
if @has_type_checker
|
98
98
|
notifications << Notification.window_log_message(
|
99
99
|
"Ruby LSP detected this is a Sorbet project and will defer to the Sorbet LSP for some functionality",
|
data/lib/ruby_lsp/internal.rb
CHANGED
@@ -36,6 +36,7 @@ require "ruby_lsp/node_context"
|
|
36
36
|
require "ruby_lsp/document"
|
37
37
|
require "ruby_lsp/ruby_document"
|
38
38
|
require "ruby_lsp/erb_document"
|
39
|
+
require "ruby_lsp/rbs_document"
|
39
40
|
require "ruby_lsp/store"
|
40
41
|
require "ruby_lsp/addon"
|
41
42
|
require "ruby_lsp/requests/support/rubocop_runner"
|
@@ -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(
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module RubyLsp
|
5
|
+
class RBSDocument < Document
|
6
|
+
extend T::Sig
|
7
|
+
extend T::Generic
|
8
|
+
|
9
|
+
ParseResultType = type_member { { fixed: T::Array[RBS::AST::Declarations::Base] } }
|
10
|
+
|
11
|
+
sig { params(source: String, version: Integer, uri: URI::Generic, encoding: Encoding).void }
|
12
|
+
def initialize(source:, version:, uri:, encoding: Encoding::UTF_8)
|
13
|
+
@syntax_error = T.let(false, T::Boolean)
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
sig { override.returns(ParseResultType) }
|
18
|
+
def parse
|
19
|
+
return @parse_result unless @needs_parsing
|
20
|
+
|
21
|
+
@needs_parsing = false
|
22
|
+
|
23
|
+
_, _, declarations = RBS::Parser.parse_signature(@source)
|
24
|
+
@syntax_error = false
|
25
|
+
@parse_result = declarations
|
26
|
+
rescue RBS::ParsingError
|
27
|
+
@syntax_error = true
|
28
|
+
@parse_result
|
29
|
+
end
|
30
|
+
|
31
|
+
sig { override.returns(T::Boolean) }
|
32
|
+
def syntax_error?
|
33
|
+
@syntax_error
|
34
|
+
end
|
35
|
+
|
36
|
+
sig { override.returns(LanguageId) }
|
37
|
+
def language_id
|
38
|
+
LanguageId::RBS
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -112,7 +112,7 @@ module RubyLsp
|
|
112
112
|
extracted_source = T.must(@document.source[start_index...end_index])
|
113
113
|
|
114
114
|
# Find the closest statements node, so that we place the refactor in a valid position
|
115
|
-
node_context =
|
115
|
+
node_context = RubyDocument
|
116
116
|
.locate(@document.parse_result.value, start_index, node_types: [Prism::StatementsNode, Prism::BlockNode])
|
117
117
|
|
118
118
|
closest_statements = node_context.node
|
@@ -206,28 +206,44 @@ module RubyLsp
|
|
206
206
|
extracted_source = T.must(@document.source[start_index...end_index])
|
207
207
|
|
208
208
|
# Find the closest method declaration node, so that we place the refactor in a valid position
|
209
|
-
node_context =
|
210
|
-
|
211
|
-
return Error::InvalidTargetRange
|
209
|
+
node_context = RubyDocument.locate(@document.parse_result.value, start_index, node_types: [Prism::DefNode])
|
210
|
+
closest_node = node_context.node
|
211
|
+
return Error::InvalidTargetRange unless closest_node
|
212
212
|
|
213
|
-
|
214
|
-
|
213
|
+
target_range = if closest_node.is_a?(Prism::DefNode)
|
214
|
+
end_keyword_loc = closest_node.end_keyword_loc
|
215
|
+
return Error::InvalidTargetRange unless end_keyword_loc
|
215
216
|
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
target_range = {
|
220
|
-
start: { line: end_line, character: character },
|
221
|
-
end: { line: end_line, character: character },
|
222
|
-
}
|
217
|
+
end_line = end_keyword_loc.end_line - 1
|
218
|
+
character = end_keyword_loc.end_column
|
219
|
+
indentation = " " * end_keyword_loc.start_column
|
223
220
|
|
224
|
-
|
221
|
+
new_method_source = <<~RUBY.chomp
|
225
222
|
|
226
223
|
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
224
|
+
#{indentation}def #{NEW_METHOD_NAME}
|
225
|
+
#{indentation} #{extracted_source}
|
226
|
+
#{indentation}end
|
227
|
+
RUBY
|
228
|
+
|
229
|
+
{
|
230
|
+
start: { line: end_line, character: character },
|
231
|
+
end: { line: end_line, character: character },
|
232
|
+
}
|
233
|
+
else
|
234
|
+
new_method_source = <<~RUBY
|
235
|
+
#{indentation}def #{NEW_METHOD_NAME}
|
236
|
+
#{indentation} #{extracted_source.gsub("\n", "\n ")}
|
237
|
+
#{indentation}end
|
238
|
+
|
239
|
+
RUBY
|
240
|
+
|
241
|
+
line = [0, source_range.dig(:start, :line) - 1].max
|
242
|
+
{
|
243
|
+
start: { line: line, character: source_range.dig(:start, :character) },
|
244
|
+
end: { line: line, character: source_range.dig(:start, :character) },
|
245
|
+
}
|
246
|
+
end
|
231
247
|
|
232
248
|
Interface::CodeAction.new(
|
233
249
|
title: CodeActions::EXTRACT_TO_METHOD_TITLE,
|
@@ -46,7 +46,7 @@ module RubyLsp
|
|
46
46
|
|
47
47
|
sig do
|
48
48
|
params(
|
49
|
-
document:
|
49
|
+
document: T.any(RubyDocument, ERBDocument),
|
50
50
|
global_state: GlobalState,
|
51
51
|
params: T::Hash[Symbol, T.untyped],
|
52
52
|
sorbet_level: RubyDocument::SorbetLevel,
|
@@ -60,7 +60,7 @@ module RubyLsp
|
|
60
60
|
# Completion always receives the position immediately after the character that was just typed. Here we adjust it
|
61
61
|
# back by 1, so that we find the right node
|
62
62
|
char_position = document.create_scanner.find_char_position(params[:position]) - 1
|
63
|
-
node_context =
|
63
|
+
node_context = RubyDocument.locate(
|
64
64
|
document.parse_result.value,
|
65
65
|
char_position,
|
66
66
|
node_types: [
|
@@ -32,7 +32,7 @@ module RubyLsp
|
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
35
|
-
sig { params(global_state: GlobalState, document:
|
35
|
+
sig { params(global_state: GlobalState, document: RubyDocument).void }
|
36
36
|
def initialize(global_state, document)
|
37
37
|
super()
|
38
38
|
@active_linters = T.let(global_state.active_linters, T::Array[Support::Formatter])
|