ruby-lsp 0.17.13 → 0.17.15
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 +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])
|