ruby-lsp 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/exe/ruby-lsp +1 -1
- data/lib/core_ext/uri.rb +4 -4
- data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +89 -14
- data/lib/ruby_indexer/lib/ruby_indexer/index.rb +25 -0
- data/lib/ruby_indexer/lib/ruby_indexer/visitor.rb +17 -15
- data/lib/ruby_indexer/ruby_indexer.rb +1 -0
- data/lib/ruby_indexer/test/classes_and_modules_test.rb +22 -6
- data/lib/ruby_indexer/test/configuration_test.rb +79 -0
- data/lib/ruby_indexer/test/constant_test.rb +4 -4
- data/lib/ruby_indexer/test/index_test.rb +35 -0
- data/lib/ruby_lsp/executor.rb +42 -12
- data/lib/ruby_lsp/requests/definition.rb +14 -6
- data/lib/ruby_lsp/requests/hover.rb +53 -30
- data/lib/ruby_lsp/requests/support/dependency_detector.rb +2 -0
- data/lib/ruby_lsp/requests/workspace_symbol.rb +86 -0
- data/lib/ruby_lsp/requests.rb +1 -0
- data/lib/ruby_lsp/setup_bundler.rb +15 -3
- metadata +13 -7
- data/lib/ruby_lsp/requests/support/rails_document_client.rb +0 -122
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3dd7fa2d26430cf1974e1d94bbfa860166dd964ffe9268d9b307b323baa86954
|
4
|
+
data.tar.gz: 6183306fd3d34396de880184fe9a09cdafccd6cb071b7927e458f658d9397221
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ed5e0524b0001497cd857dad621a6b072434375ef2be8583e12ddfcd2b7d104b279ab7ea026db09e563ddd908d7d575da682bf6a30244cd6aca68da6b4ee2ad6
|
7
|
+
data.tar.gz: 8de1bdb11dfb12c34129e3fddeae89c6b3700a74cb6483424523df9584a28a8b52499fade45d7c7c047ee517ed53a5a4f54b9fcc04a386a17b092999a264563a
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.9.0
|
data/exe/ruby-lsp
CHANGED
data/lib/core_ext/uri.rb
CHANGED
@@ -6,8 +6,8 @@ module URI
|
|
6
6
|
class << self
|
7
7
|
extend T::Sig
|
8
8
|
|
9
|
-
sig { params(path: String, scheme: String).returns(URI::Generic) }
|
10
|
-
def from_path(path:, scheme: "file")
|
9
|
+
sig { params(path: String, fragment: T.nilable(String), scheme: String).returns(URI::Generic) }
|
10
|
+
def from_path(path:, fragment: nil, scheme: "file")
|
11
11
|
# On Windows, if the path begins with the disk name, we need to add a leading slash to make it a valid URI
|
12
12
|
escaped_path = if /^[A-Z]:/i.match?(path)
|
13
13
|
DEFAULT_PARSER.escape("/#{path}")
|
@@ -15,7 +15,7 @@ module URI
|
|
15
15
|
DEFAULT_PARSER.escape(path)
|
16
16
|
end
|
17
17
|
|
18
|
-
build(scheme: scheme, path: escaped_path)
|
18
|
+
build(scheme: scheme, path: escaped_path, fragment: fragment)
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
@@ -26,7 +26,7 @@ module URI
|
|
26
26
|
parsed_path = path
|
27
27
|
return unless parsed_path
|
28
28
|
|
29
|
-
unescaped_path =
|
29
|
+
unescaped_path = DEFAULT_PARSER.unescape(parsed_path)
|
30
30
|
|
31
31
|
# On Windows, when we're getting the file system path back from the URI, we need to remove the leading forward
|
32
32
|
# slash
|
@@ -11,20 +11,40 @@ module RubyIndexer
|
|
11
11
|
"included_gems" => Array,
|
12
12
|
"excluded_patterns" => Array,
|
13
13
|
"included_patterns" => Array,
|
14
|
+
"excluded_magic_comments" => Array,
|
14
15
|
}.freeze,
|
15
|
-
T::Hash[String, T
|
16
|
+
T::Hash[String, T.untyped],
|
16
17
|
)
|
17
18
|
|
18
19
|
sig { void }
|
19
20
|
def initialize
|
20
|
-
|
21
|
+
excluded_gem_names = Bundler.definition.dependencies.filter_map do |dependency|
|
21
22
|
dependency.name if dependency.groups == [:development]
|
22
23
|
end
|
23
24
|
|
24
|
-
@excluded_gems = T.let(
|
25
|
+
@excluded_gems = T.let(excluded_gem_names, T::Array[String])
|
25
26
|
@included_gems = T.let([], T::Array[String])
|
26
|
-
@excluded_patterns = T.let(["
|
27
|
+
@excluded_patterns = T.let(["**/*_test.rb"], T::Array[String])
|
28
|
+
path = Bundler.settings["path"]
|
29
|
+
@excluded_patterns << "#{File.expand_path(path, Dir.pwd)}/**/*.rb" if path
|
30
|
+
|
27
31
|
@included_patterns = T.let(["#{Dir.pwd}/**/*.rb"], T::Array[String])
|
32
|
+
@excluded_magic_comments = T.let(
|
33
|
+
[
|
34
|
+
"frozen_string_literal:",
|
35
|
+
"typed:",
|
36
|
+
"compiled:",
|
37
|
+
"encoding:",
|
38
|
+
"shareable_constant_value:",
|
39
|
+
"warn_indent:",
|
40
|
+
"rubocop:",
|
41
|
+
"nodoc:",
|
42
|
+
"doc:",
|
43
|
+
"coding:",
|
44
|
+
"warn_past_scope:",
|
45
|
+
],
|
46
|
+
T::Array[String],
|
47
|
+
)
|
28
48
|
end
|
29
49
|
|
30
50
|
sig { void }
|
@@ -43,26 +63,80 @@ module RubyIndexer
|
|
43
63
|
|
44
64
|
sig { returns(T::Array[String]) }
|
45
65
|
def files_to_index
|
46
|
-
|
66
|
+
excluded_gems = @excluded_gems - @included_gems
|
67
|
+
locked_gems = Bundler.locked_gems&.specs
|
47
68
|
|
48
|
-
|
49
|
-
|
50
|
-
end
|
69
|
+
# NOTE: indexing the patterns (both included and excluded) needs to happen before indexing gems, otherwise we risk
|
70
|
+
# having duplicates if BUNDLE_PATH is set to a folder inside the project structure
|
51
71
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
warn("Gem #{gem_name} is excluded in .index.yml, but that gem was not found in the bundle")
|
72
|
+
# Add user specified patterns
|
73
|
+
files_to_index = @included_patterns.flat_map do |pattern|
|
74
|
+
Dir.glob(pattern, File::FNM_PATHNAME | File::FNM_EXTGLOB)
|
56
75
|
end
|
57
76
|
|
77
|
+
# Remove user specified patterns
|
58
78
|
files_to_index.reject! do |path|
|
59
|
-
@excluded_patterns.any?
|
60
|
-
|
79
|
+
@excluded_patterns.any? do |pattern|
|
80
|
+
File.fnmatch?(pattern, path, File::FNM_PATHNAME | File::FNM_EXTGLOB)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Add default gems to the list of files to be indexed
|
85
|
+
Dir.glob("#{RbConfig::CONFIG["rubylibdir"]}/*").each do |default_path|
|
86
|
+
# The default_path might be a Ruby file or a folder with the gem's name. For example:
|
87
|
+
# bundler/
|
88
|
+
# bundler.rb
|
89
|
+
# psych/
|
90
|
+
# psych.rb
|
91
|
+
pathname = Pathname.new(default_path)
|
92
|
+
short_name = pathname.basename.to_s.delete_suffix(".rb")
|
93
|
+
|
94
|
+
# If the gem name is excluded, then we skip it
|
95
|
+
next if excluded_gems.include?(short_name)
|
96
|
+
|
97
|
+
# If the default gem is also a part of the bundle, we skip indexing the default one and index only the one in
|
98
|
+
# the bundle, which won't be in `default_path`, but will be in `Bundler.bundle_path` instead
|
99
|
+
next if locked_gems&.any? do |locked_spec|
|
100
|
+
locked_spec.name == short_name &&
|
101
|
+
!Gem::Specification.find_by_name(short_name).full_gem_path.start_with?(RbConfig::CONFIG["rubylibprefix"])
|
102
|
+
end
|
103
|
+
|
104
|
+
if pathname.directory?
|
105
|
+
# If the default_path is a directory, we index all the Ruby files in it
|
106
|
+
files_to_index.concat(Dir.glob("#{default_path}/**/*.rb", File::FNM_PATHNAME | File::FNM_EXTGLOB))
|
107
|
+
else
|
108
|
+
# If the default_path is a Ruby file, we index it
|
109
|
+
files_to_index << default_path
|
110
|
+
end
|
61
111
|
end
|
112
|
+
|
113
|
+
# Add the locked gems to the list of files to be indexed
|
114
|
+
locked_gems&.each do |lazy_spec|
|
115
|
+
next if excluded_gems.include?(lazy_spec.name)
|
116
|
+
|
117
|
+
spec = Gem::Specification.find_by_name(lazy_spec.name)
|
118
|
+
|
119
|
+
# When working on a gem, it will be included in the locked_gems list. Since these are the project's own files,
|
120
|
+
# we have already included and handled exclude patterns for it and should not re-include or it'll lead to
|
121
|
+
# duplicates or accidentally ignoring exclude patterns
|
122
|
+
next if spec.full_gem_path == Dir.pwd
|
123
|
+
|
124
|
+
files_to_index.concat(Dir.glob("#{spec.full_gem_path}/{#{spec.require_paths.join(",")}}/**/*.rb"))
|
125
|
+
rescue Gem::MissingSpecError
|
126
|
+
# If a gem is scoped only to some specific platform, then its dependencies may not be installed either, but they
|
127
|
+
# are still listed in locked_gems. We can't index them because they are not installed for the platform, so we
|
128
|
+
# just ignore if they're missing
|
129
|
+
end
|
130
|
+
|
62
131
|
files_to_index.uniq!
|
63
132
|
files_to_index
|
64
133
|
end
|
65
134
|
|
135
|
+
sig { returns(Regexp) }
|
136
|
+
def magic_comment_regex
|
137
|
+
/^\s*#\s*#{@excluded_magic_comments.join("|")}/
|
138
|
+
end
|
139
|
+
|
66
140
|
private
|
67
141
|
|
68
142
|
sig { params(config: T::Hash[String, T.untyped]).void }
|
@@ -86,6 +160,7 @@ module RubyIndexer
|
|
86
160
|
@included_gems.concat(config["included_gems"]) if config["included_gems"]
|
87
161
|
@excluded_patterns.concat(config["excluded_patterns"]) if config["excluded_patterns"]
|
88
162
|
@included_patterns.concat(config["included_patterns"]) if config["included_patterns"]
|
163
|
+
@excluded_magic_comments.concat(config["excluded_magic_comments"]) if config["excluded_magic_comments"]
|
89
164
|
end
|
90
165
|
end
|
91
166
|
end
|
@@ -5,6 +5,9 @@ module RubyIndexer
|
|
5
5
|
class Index
|
6
6
|
extend T::Sig
|
7
7
|
|
8
|
+
# The minimum Jaro-Winkler similarity score for an entry to be considered a match for a given fuzzy search query
|
9
|
+
ENTRY_SIMILARITY_THRESHOLD = 0.7
|
10
|
+
|
8
11
|
sig { void }
|
9
12
|
def initialize
|
10
13
|
# Holds all entries in the index using the following format:
|
@@ -50,6 +53,21 @@ module RubyIndexer
|
|
50
53
|
@entries[fully_qualified_name.delete_prefix("::")]
|
51
54
|
end
|
52
55
|
|
56
|
+
# Fuzzy searches index entries based on Jaro-Winkler similarity. If no query is provided, all entries are returned
|
57
|
+
sig { params(query: T.nilable(String)).returns(T::Array[Entry]) }
|
58
|
+
def fuzzy_search(query)
|
59
|
+
return @entries.flat_map { |_name, entries| entries } unless query
|
60
|
+
|
61
|
+
normalized_query = query.gsub("::", "").downcase
|
62
|
+
|
63
|
+
results = @entries.filter_map do |name, entries|
|
64
|
+
similarity = DidYouMean::JaroWinkler.distance(name.gsub("::", "").downcase, normalized_query)
|
65
|
+
[entries, -similarity] if similarity > ENTRY_SIMILARITY_THRESHOLD
|
66
|
+
end
|
67
|
+
results.sort_by!(&:last)
|
68
|
+
results.flat_map(&:first)
|
69
|
+
end
|
70
|
+
|
53
71
|
# Try to find the entry based on the nesting from the most specific to the least specific. For example, if we have
|
54
72
|
# the nesting as ["Foo", "Bar"] and the name as "Baz", we will try to find it in this order:
|
55
73
|
# 1. Foo::Bar::Baz
|
@@ -77,6 +95,8 @@ module RubyIndexer
|
|
77
95
|
content = source || File.read(path)
|
78
96
|
visitor = IndexVisitor.new(self, YARP.parse(content), path)
|
79
97
|
visitor.run
|
98
|
+
rescue Errno::EISDIR
|
99
|
+
# If `path` is a directory, just ignore it and continue indexing
|
80
100
|
end
|
81
101
|
|
82
102
|
class Entry
|
@@ -102,6 +122,11 @@ module RubyIndexer
|
|
102
122
|
@comments = comments
|
103
123
|
end
|
104
124
|
|
125
|
+
sig { returns(String) }
|
126
|
+
def file_name
|
127
|
+
File.basename(@file_path)
|
128
|
+
end
|
129
|
+
|
105
130
|
class Namespace < Entry
|
106
131
|
sig { returns(String) }
|
107
132
|
def short_name
|
@@ -35,9 +35,9 @@ module RubyIndexer
|
|
35
35
|
add_index_entry(node, Index::Entry::Class)
|
36
36
|
when YARP::ModuleNode
|
37
37
|
add_index_entry(node, Index::Entry::Module)
|
38
|
-
when YARP::ConstantWriteNode, YARP::
|
38
|
+
when YARP::ConstantWriteNode, YARP::ConstantOrWriteNode
|
39
39
|
add_constant(node)
|
40
|
-
when YARP::ConstantPathWriteNode, YARP::
|
40
|
+
when YARP::ConstantPathWriteNode, YARP::ConstantPathOrWriteNode
|
41
41
|
add_constant_with_path(node)
|
42
42
|
end
|
43
43
|
end
|
@@ -52,7 +52,7 @@ module RubyIndexer
|
|
52
52
|
|
53
53
|
sig do
|
54
54
|
params(
|
55
|
-
node: T.any(YARP::ConstantWriteNode, YARP::
|
55
|
+
node: T.any(YARP::ConstantWriteNode, YARP::ConstantOrWriteNode),
|
56
56
|
).void
|
57
57
|
end
|
58
58
|
def add_constant(node)
|
@@ -62,7 +62,7 @@ module RubyIndexer
|
|
62
62
|
|
63
63
|
sig do
|
64
64
|
params(
|
65
|
-
node: T.any(YARP::ConstantPathWriteNode, YARP::
|
65
|
+
node: T.any(YARP::ConstantPathWriteNode, YARP::ConstantPathOrWriteNode),
|
66
66
|
).void
|
67
67
|
end
|
68
68
|
def add_constant_with_path(node)
|
@@ -70,10 +70,8 @@ module RubyIndexer
|
|
70
70
|
return unless node.target.parent.nil? || node.target.parent.is_a?(YARP::ConstantReadNode)
|
71
71
|
|
72
72
|
name = node.target.location.slice
|
73
|
-
fully_qualified_name = name.start_with?("::") ? name.delete_prefix("::") : fully_qualify_name(name)
|
74
|
-
|
75
73
|
comments = collect_comments(node)
|
76
|
-
@index << Index::Entry::Constant.new(
|
74
|
+
@index << Index::Entry::Constant.new(fully_qualify_name(name), @file_path, node.location, comments)
|
77
75
|
end
|
78
76
|
|
79
77
|
sig { params(node: T.any(YARP::ClassNode, YARP::ModuleNode), klass: T.class_of(Index::Entry)).void }
|
@@ -84,11 +82,8 @@ module RubyIndexer
|
|
84
82
|
return visit_child_nodes(node)
|
85
83
|
end
|
86
84
|
|
87
|
-
fully_qualified_name = name.start_with?("::") ? name : fully_qualify_name(name)
|
88
|
-
name.delete_prefix!("::")
|
89
|
-
|
90
85
|
comments = collect_comments(node)
|
91
|
-
@index << klass.new(
|
86
|
+
@index << klass.new(fully_qualify_name(name), @file_path, node.location, comments)
|
92
87
|
@stack << name
|
93
88
|
visit_child_nodes(node)
|
94
89
|
@stack.pop
|
@@ -105,7 +100,12 @@ module RubyIndexer
|
|
105
100
|
comment = @comments_by_line[line]
|
106
101
|
break unless comment
|
107
102
|
|
108
|
-
|
103
|
+
comment_content = comment.location.slice.chomp
|
104
|
+
next if comment_content.match?(RubyIndexer.configuration.magic_comment_regex)
|
105
|
+
|
106
|
+
comment_content.delete_prefix!("#")
|
107
|
+
comment_content.delete_prefix!(" ")
|
108
|
+
comments.unshift(comment_content)
|
109
109
|
end
|
110
110
|
|
111
111
|
comments
|
@@ -113,9 +113,11 @@ module RubyIndexer
|
|
113
113
|
|
114
114
|
sig { params(name: String).returns(String) }
|
115
115
|
def fully_qualify_name(name)
|
116
|
-
|
117
|
-
|
118
|
-
|
116
|
+
if @stack.empty? || name.start_with?("::")
|
117
|
+
name
|
118
|
+
else
|
119
|
+
"#{@stack.join("::")}::#{name}"
|
120
|
+
end.delete_prefix("::")
|
119
121
|
end
|
120
122
|
end
|
121
123
|
end
|
@@ -162,10 +162,10 @@ module RubyIndexer
|
|
162
162
|
RUBY
|
163
163
|
|
164
164
|
foo_entry = @index["Foo"].first
|
165
|
-
assert_equal("
|
165
|
+
assert_equal("This is a Foo comment\nThis is another Foo comment", foo_entry.comments.join("\n"))
|
166
166
|
|
167
167
|
bar_entry = @index["Bar"].first
|
168
|
-
assert_equal("
|
168
|
+
assert_equal("This Bar comment has 1 line padding", bar_entry.comments.join("\n"))
|
169
169
|
end
|
170
170
|
|
171
171
|
def test_comments_can_be_attached_to_a_namespaced_class
|
@@ -179,10 +179,10 @@ module RubyIndexer
|
|
179
179
|
RUBY
|
180
180
|
|
181
181
|
foo_entry = @index["Foo"].first
|
182
|
-
assert_equal("
|
182
|
+
assert_equal("This is a Foo comment\nThis is another Foo comment", foo_entry.comments.join("\n"))
|
183
183
|
|
184
184
|
bar_entry = @index["Foo::Bar"].first
|
185
|
-
assert_equal("
|
185
|
+
assert_equal("This is a Bar comment", bar_entry.comments.join("\n"))
|
186
186
|
end
|
187
187
|
|
188
188
|
def test_comments_can_be_attached_to_a_reopened_class
|
@@ -195,10 +195,26 @@ module RubyIndexer
|
|
195
195
|
RUBY
|
196
196
|
|
197
197
|
first_foo_entry = @index["Foo"][0]
|
198
|
-
assert_equal("
|
198
|
+
assert_equal("This is a Foo comment", first_foo_entry.comments.join("\n"))
|
199
199
|
|
200
200
|
second_foo_entry = @index["Foo"][1]
|
201
|
-
assert_equal("
|
201
|
+
assert_equal("This is another Foo comment", second_foo_entry.comments.join("\n"))
|
202
|
+
end
|
203
|
+
|
204
|
+
def test_comments_removes_the_leading_pound_and_space
|
205
|
+
index(<<~RUBY)
|
206
|
+
# This is a Foo comment
|
207
|
+
class Foo; end
|
208
|
+
|
209
|
+
#This is a Bar comment
|
210
|
+
class Bar; end
|
211
|
+
RUBY
|
212
|
+
|
213
|
+
first_foo_entry = @index["Foo"][0]
|
214
|
+
assert_equal("This is a Foo comment", first_foo_entry.comments.join("\n"))
|
215
|
+
|
216
|
+
second_foo_entry = @index["Bar"][0]
|
217
|
+
assert_equal("This is a Bar comment", second_foo_entry.comments.join("\n"))
|
202
218
|
end
|
203
219
|
end
|
204
220
|
end
|
@@ -15,6 +15,65 @@ module RubyIndexer
|
|
15
15
|
|
16
16
|
assert(files_to_index.none? { |path| path.include?("test/fixtures") })
|
17
17
|
assert(files_to_index.none? { |path| path.include?("minitest-reporters") })
|
18
|
+
assert(files_to_index.none? { |path| path == __FILE__ })
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_files_to_index_only_includes_gem_require_paths
|
22
|
+
@config.load_config
|
23
|
+
files_to_index = @config.files_to_index
|
24
|
+
|
25
|
+
Bundler.locked_gems.specs.each do |lazy_spec|
|
26
|
+
next if lazy_spec.name == "ruby-lsp"
|
27
|
+
|
28
|
+
spec = Gem::Specification.find_by_name(lazy_spec.name)
|
29
|
+
assert(files_to_index.none? { |path| path.start_with?("#{spec.full_gem_path}/test/") })
|
30
|
+
rescue Gem::MissingSpecError
|
31
|
+
# Transitive dependencies might be missing when running tests on Windows
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_files_to_index_does_not_include_default_gem_path_when_in_bundle
|
36
|
+
@config.load_config
|
37
|
+
files_to_index = @config.files_to_index
|
38
|
+
|
39
|
+
assert(files_to_index.none? { |path| path.start_with?("#{RbConfig::CONFIG["rubylibdir"]}/psych") })
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_files_to_index_includes_default_gems
|
43
|
+
@config.load_config
|
44
|
+
files_to_index = @config.files_to_index
|
45
|
+
|
46
|
+
assert_includes(files_to_index, "#{RbConfig::CONFIG["rubylibdir"]}/pathname.rb")
|
47
|
+
assert_includes(files_to_index, "#{RbConfig::CONFIG["rubylibdir"]}/ipaddr.rb")
|
48
|
+
assert_includes(files_to_index, "#{RbConfig::CONFIG["rubylibdir"]}/abbrev.rb")
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_files_to_index_includes_project_files
|
52
|
+
@config.load_config
|
53
|
+
files_to_index = @config.files_to_index
|
54
|
+
|
55
|
+
Dir.glob("#{Dir.pwd}/lib/**/*.rb").each do |path|
|
56
|
+
next if path.end_with?("_test.rb")
|
57
|
+
|
58
|
+
assert_includes(files_to_index, path)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_files_to_index_avoids_duplicates_if_bundle_path_is_inside_project
|
63
|
+
Bundler.settings.set_global("path", "vendor/bundle")
|
64
|
+
config = Configuration.new
|
65
|
+
config.load_config
|
66
|
+
|
67
|
+
assert_includes(config.instance_variable_get(:@excluded_patterns), "#{Dir.pwd}/vendor/bundle/**/*.rb")
|
68
|
+
ensure
|
69
|
+
Bundler.settings.set_global("path", nil)
|
70
|
+
end
|
71
|
+
|
72
|
+
def test_files_to_index_does_not_include_gems_own_installed_files
|
73
|
+
@config.load_config
|
74
|
+
files_to_index = @config.files_to_index
|
75
|
+
|
76
|
+
assert(files_to_index.none? { |path| path.start_with?(Bundler.bundle_path.join("gems", "ruby-lsp").to_s) })
|
18
77
|
end
|
19
78
|
|
20
79
|
def test_paths_are_unique
|
@@ -31,5 +90,25 @@ module RubyIndexer
|
|
31
90
|
@config.load_config
|
32
91
|
end
|
33
92
|
end
|
93
|
+
|
94
|
+
def test_magic_comments_regex
|
95
|
+
regex = RubyIndexer.configuration.magic_comment_regex
|
96
|
+
|
97
|
+
[
|
98
|
+
"# frozen_string_literal:",
|
99
|
+
"# typed:",
|
100
|
+
"# compiled:",
|
101
|
+
"# encoding:",
|
102
|
+
"# shareable_constant_value:",
|
103
|
+
"# warn_indent:",
|
104
|
+
"# rubocop:",
|
105
|
+
"# nodoc:",
|
106
|
+
"# doc:",
|
107
|
+
"# coding:",
|
108
|
+
"# warn_past_scope:",
|
109
|
+
].each do |comment|
|
110
|
+
assert_match(regex, comment)
|
111
|
+
end
|
112
|
+
end
|
34
113
|
end
|
35
114
|
end
|
@@ -84,16 +84,16 @@ module RubyIndexer
|
|
84
84
|
RUBY
|
85
85
|
|
86
86
|
foo_comment = @index["FOO"].first.comments.join("\n")
|
87
|
-
assert_equal("
|
87
|
+
assert_equal("FOO comment", foo_comment)
|
88
88
|
|
89
89
|
a_foo_comment = @index["A::FOO"].first.comments.join("\n")
|
90
|
-
assert_equal("
|
90
|
+
assert_equal("A::FOO comment", a_foo_comment)
|
91
91
|
|
92
92
|
bar_comment = @index["BAR"].first.comments.join("\n")
|
93
|
-
assert_equal("
|
93
|
+
assert_equal("::BAR comment", bar_comment)
|
94
94
|
|
95
95
|
a_baz_comment = @index["A::BAZ"].first.comments.join("\n")
|
96
|
-
assert_equal("
|
96
|
+
assert_equal("A::BAZ comment", a_baz_comment)
|
97
97
|
end
|
98
98
|
|
99
99
|
def test_variable_path_constants_are_ignored
|
@@ -90,5 +90,40 @@ module RubyIndexer
|
|
90
90
|
refute_empty(entries)
|
91
91
|
assert_equal("Foo::Baz::Something", entries.first.name)
|
92
92
|
end
|
93
|
+
|
94
|
+
def test_fuzzy_search
|
95
|
+
@index.index_single("/fake/path/foo.rb", <<~RUBY)
|
96
|
+
class Bar; end
|
97
|
+
|
98
|
+
module Foo
|
99
|
+
class Bar
|
100
|
+
end
|
101
|
+
|
102
|
+
class Baz
|
103
|
+
class Something
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
RUBY
|
108
|
+
|
109
|
+
result = @index.fuzzy_search("Bar")
|
110
|
+
assert_equal(1, result.length)
|
111
|
+
assert_equal(@index["Bar"].first, result.first)
|
112
|
+
|
113
|
+
result = @index.fuzzy_search("foobarsomeking")
|
114
|
+
assert_equal(5, result.length)
|
115
|
+
assert_equal(["Foo::Baz::Something", "Foo::Bar", "Foo::Baz", "Foo", "Bar"], result.map(&:name))
|
116
|
+
|
117
|
+
result = @index.fuzzy_search("FooBaz")
|
118
|
+
assert_equal(4, result.length)
|
119
|
+
assert_equal(["Foo::Baz", "Foo::Bar", "Foo", "Foo::Baz::Something"], result.map(&:name))
|
120
|
+
end
|
121
|
+
|
122
|
+
def test_index_single_ignores_directories
|
123
|
+
FileUtils.mkdir("lib/this_is_a_dir.rb")
|
124
|
+
@index.index_single("lib/this_is_a_dir.rb")
|
125
|
+
ensure
|
126
|
+
FileUtils.rm_r("lib/this_is_a_dir.rb")
|
127
|
+
end
|
93
128
|
end
|
94
129
|
end
|
data/lib/ruby_lsp/executor.rb
CHANGED
@@ -58,14 +58,7 @@ module RubyLsp
|
|
58
58
|
warn(errored_extensions.map(&:backtraces).join("\n\n"))
|
59
59
|
end
|
60
60
|
|
61
|
-
|
62
|
-
# The begin progress invocation happens during `initialize`, so that the notification is sent before we are
|
63
|
-
# stuck indexing files
|
64
|
-
RubyIndexer.configuration.load_config
|
65
|
-
@index.index_all
|
66
|
-
end_progress("indexing-progress")
|
67
|
-
end
|
68
|
-
|
61
|
+
perform_initial_indexing
|
69
62
|
check_formatter_is_available
|
70
63
|
|
71
64
|
warn("Ruby LSP is ready")
|
@@ -180,6 +173,8 @@ module RubyLsp
|
|
180
173
|
definition(uri, request.dig(:params, :position))
|
181
174
|
when "workspace/didChangeWatchedFiles"
|
182
175
|
did_change_watched_files(request.dig(:params, :changes))
|
176
|
+
when "workspace/symbol"
|
177
|
+
workspace_symbol(request.dig(:params, :query))
|
183
178
|
when "rubyLsp/textDocument/showSyntaxTree"
|
184
179
|
show_syntax_tree(uri, request.dig(:params, :range))
|
185
180
|
end
|
@@ -207,6 +202,36 @@ module RubyLsp
|
|
207
202
|
VOID
|
208
203
|
end
|
209
204
|
|
205
|
+
sig { void }
|
206
|
+
def perform_initial_indexing
|
207
|
+
return unless @store.experimental_features
|
208
|
+
|
209
|
+
# The begin progress invocation happens during `initialize`, so that the notification is sent before we are
|
210
|
+
# stuck indexing files
|
211
|
+
RubyIndexer.configuration.load_config
|
212
|
+
|
213
|
+
begin
|
214
|
+
@index.index_all
|
215
|
+
rescue StandardError => error
|
216
|
+
@message_queue << Notification.new(
|
217
|
+
message: "window/showMessage",
|
218
|
+
params: Interface::ShowMessageParams.new(
|
219
|
+
type: Constant::MessageType::ERROR,
|
220
|
+
message: "Error while indexing: #{error.message}",
|
221
|
+
),
|
222
|
+
)
|
223
|
+
end
|
224
|
+
|
225
|
+
# Always end the progress notification even if indexing failed or else it never goes away and the user has no way
|
226
|
+
# of dismissing it
|
227
|
+
end_progress("indexing-progress")
|
228
|
+
end
|
229
|
+
|
230
|
+
sig { params(query: T.nilable(String)).returns(T::Array[Interface::WorkspaceSymbol]) }
|
231
|
+
def workspace_symbol(query)
|
232
|
+
Requests::WorkspaceSymbol.new(query, @index).run
|
233
|
+
end
|
234
|
+
|
210
235
|
sig { params(uri: URI::Generic, range: T.nilable(Document::RangeShape)).returns({ ast: String }) }
|
211
236
|
def show_syntax_tree(uri, range)
|
212
237
|
{ ast: Requests::ShowSyntaxTree.new(@store.get(uri), range).run }
|
@@ -252,16 +277,20 @@ module RubyLsp
|
|
252
277
|
document = @store.get(uri)
|
253
278
|
return if document.syntax_error?
|
254
279
|
|
255
|
-
target, parent = document.locate_node(
|
280
|
+
target, parent, nesting = document.locate_node(
|
281
|
+
position,
|
282
|
+
node_types: Requests::Hover::ALLOWED_TARGETS,
|
283
|
+
)
|
256
284
|
|
257
|
-
if
|
258
|
-
Requests::Hover::ALLOWED_TARGETS.include?(
|
285
|
+
if (Requests::Hover::ALLOWED_TARGETS.include?(parent.class) &&
|
286
|
+
!Requests::Hover::ALLOWED_TARGETS.include?(target.class)) ||
|
287
|
+
(parent.is_a?(SyntaxTree::ConstPathRef) && target.is_a?(SyntaxTree::Const))
|
259
288
|
target = parent
|
260
289
|
end
|
261
290
|
|
262
291
|
# Instantiate all listeners
|
263
292
|
emitter = EventEmitter.new
|
264
|
-
hover = Requests::Hover.new(emitter, @message_queue)
|
293
|
+
hover = Requests::Hover.new(@index, nesting, emitter, @message_queue)
|
265
294
|
|
266
295
|
# Emit events for all listeners
|
267
296
|
emitter.emit_for_target(target)
|
@@ -667,6 +696,7 @@ module RubyLsp
|
|
667
696
|
completion_provider: completion_provider,
|
668
697
|
code_lens_provider: code_lens_provider,
|
669
698
|
definition_provider: enabled_features["definition"],
|
699
|
+
workspace_symbol_provider: enabled_features["workspaceSymbol"],
|
670
700
|
),
|
671
701
|
)
|
672
702
|
end
|
@@ -9,12 +9,13 @@ module RubyLsp
|
|
9
9
|
# request](https://microsoft.github.io/language-server-protocol/specification#textDocument_definition) jumps to the
|
10
10
|
# definition of the symbol under the cursor.
|
11
11
|
#
|
12
|
-
# Currently, only jumping to required files is supported.
|
12
|
+
# Currently, only jumping to classes, modules and required files is supported.
|
13
13
|
#
|
14
14
|
# # Example
|
15
15
|
#
|
16
16
|
# ```ruby
|
17
17
|
# require "some_gem/file" # <- Request go to definition on this string will take you to the file
|
18
|
+
# Product.new # <- Request go to definition on this class name will take you to its declaration.
|
18
19
|
# ```
|
19
20
|
class Definition < Listener
|
20
21
|
extend T::Sig
|
@@ -25,8 +26,6 @@ module RubyLsp
|
|
25
26
|
sig { override.returns(ResponseType) }
|
26
27
|
attr_reader :response
|
27
28
|
|
28
|
-
HAS_TYPECHECKER = T.let(DependencyDetector.typechecker?, T::Boolean)
|
29
|
-
|
30
29
|
sig do
|
31
30
|
params(
|
32
31
|
uri: URI::Generic,
|
@@ -107,17 +106,26 @@ module RubyLsp
|
|
107
106
|
entries = @index.resolve(value, @nesting)
|
108
107
|
return unless entries
|
109
108
|
|
110
|
-
|
109
|
+
bundle_path = begin
|
110
|
+
Bundler.bundle_path.to_s
|
111
|
+
rescue Bundler::GemfileNotFound
|
112
|
+
nil
|
113
|
+
end
|
111
114
|
|
112
115
|
@response = entries.filter_map do |entry|
|
113
116
|
location = entry.location
|
114
117
|
# If the project has Sorbet, then we only want to handle go to definition for constants defined in gems, as an
|
115
118
|
# additional behavior on top of jumping to RBIs. Sorbet can already handle go to definition for all constants
|
116
119
|
# in the project, even if the files are typed false
|
117
|
-
|
120
|
+
file_path = entry.file_path
|
121
|
+
if DependencyDetector::HAS_TYPECHECKER && bundle_path && !file_path.start_with?(bundle_path) &&
|
122
|
+
!file_path.start_with?(RbConfig::CONFIG["rubylibdir"])
|
123
|
+
|
124
|
+
next
|
125
|
+
end
|
118
126
|
|
119
127
|
Interface::Location.new(
|
120
|
-
uri: URI::Generic.from_path(path:
|
128
|
+
uri: URI::Generic.from_path(path: file_path).to_s,
|
121
129
|
range: Interface::Range.new(
|
122
130
|
start: Interface::Position.new(line: location.start_line - 1, character: location.start_column),
|
123
131
|
end: Interface::Position.new(line: location.end_line - 1, character: location.end_column),
|
@@ -3,19 +3,15 @@
|
|
3
3
|
|
4
4
|
module RubyLsp
|
5
5
|
module Requests
|
6
|
-
# 
|
7
7
|
#
|
8
8
|
# The [hover request](https://microsoft.github.io/language-server-protocol/specification#textDocument_hover)
|
9
|
-
#
|
10
|
-
# It currently only supports Rails' documentation: when hovering over Rails DSLs/constants under certain paths,
|
11
|
-
# like `before_save :callback` in `models/post.rb`, it generates a link to `before_save`'s API documentation.
|
9
|
+
# displays the documentation for the symbol currently under the cursor.
|
12
10
|
#
|
13
11
|
# # Example
|
14
12
|
#
|
15
13
|
# ```ruby
|
16
|
-
# class
|
17
|
-
# before_save :do_something # when hovering on before_save, the link will be rendered
|
18
|
-
# end
|
14
|
+
# String # -> Hovering over the class reference will show all declaration locations and the documentation
|
19
15
|
# ```
|
20
16
|
class Hover < Listener
|
21
17
|
extend T::Sig
|
@@ -25,6 +21,7 @@ module RubyLsp
|
|
25
21
|
|
26
22
|
ALLOWED_TARGETS = T.let(
|
27
23
|
[
|
24
|
+
SyntaxTree::Const,
|
28
25
|
SyntaxTree::Command,
|
29
26
|
SyntaxTree::CallNode,
|
30
27
|
SyntaxTree::ConstPathRef,
|
@@ -35,15 +32,24 @@ module RubyLsp
|
|
35
32
|
sig { override.returns(ResponseType) }
|
36
33
|
attr_reader :response
|
37
34
|
|
38
|
-
sig
|
39
|
-
|
40
|
-
|
35
|
+
sig do
|
36
|
+
params(
|
37
|
+
index: RubyIndexer::Index,
|
38
|
+
nesting: T::Array[String],
|
39
|
+
emitter: EventEmitter,
|
40
|
+
message_queue: Thread::Queue,
|
41
|
+
).void
|
42
|
+
end
|
43
|
+
def initialize(index, nesting, emitter, message_queue)
|
44
|
+
super(emitter, message_queue)
|
41
45
|
|
46
|
+
@nesting = nesting
|
47
|
+
@index = index
|
42
48
|
@external_listeners.concat(
|
43
49
|
Extension.extensions.filter_map { |ext| ext.create_hover_listener(emitter, message_queue) },
|
44
50
|
)
|
45
51
|
@response = T.let(nil, ResponseType)
|
46
|
-
emitter.register(self, :
|
52
|
+
emitter.register(self, :on_const_path_ref, :on_const)
|
47
53
|
end
|
48
54
|
|
49
55
|
# Merges responses from other hover listeners
|
@@ -55,40 +61,57 @@ module RubyLsp
|
|
55
61
|
if @response.nil?
|
56
62
|
@response = other.response
|
57
63
|
else
|
58
|
-
@response.contents.value <<
|
64
|
+
@response.contents.value << "\n\n" << other_response.contents.value
|
59
65
|
end
|
60
66
|
|
61
67
|
self
|
62
68
|
end
|
63
69
|
|
64
|
-
sig { params(node: SyntaxTree::Command).void }
|
65
|
-
def on_command(node)
|
66
|
-
message = node.message
|
67
|
-
@response = generate_rails_document_link_hover(message.value, message)
|
68
|
-
end
|
69
|
-
|
70
70
|
sig { params(node: SyntaxTree::ConstPathRef).void }
|
71
71
|
def on_const_path_ref(node)
|
72
|
-
|
72
|
+
return if DependencyDetector::HAS_TYPECHECKER
|
73
|
+
|
74
|
+
name = full_constant_name(node)
|
75
|
+
generate_hover(name, node)
|
73
76
|
end
|
74
77
|
|
75
|
-
sig { params(node: SyntaxTree::
|
76
|
-
def
|
77
|
-
|
78
|
-
return if message.is_a?(Symbol)
|
78
|
+
sig { params(node: SyntaxTree::Const).void }
|
79
|
+
def on_const(node)
|
80
|
+
return if DependencyDetector::HAS_TYPECHECKER
|
79
81
|
|
80
|
-
|
82
|
+
generate_hover(node.value, node)
|
81
83
|
end
|
82
84
|
|
83
85
|
private
|
84
86
|
|
85
|
-
sig { params(name: String, node: SyntaxTree::Node).
|
86
|
-
def
|
87
|
-
|
88
|
-
return
|
87
|
+
sig { params(name: String, node: SyntaxTree::Node).void }
|
88
|
+
def generate_hover(name, node)
|
89
|
+
entries = @index.resolve(name, @nesting)
|
90
|
+
return unless entries
|
91
|
+
|
92
|
+
title = +"```ruby\n#{name}\n```"
|
93
|
+
definitions = []
|
94
|
+
content = +""
|
95
|
+
entries.each do |entry|
|
96
|
+
loc = entry.location
|
97
|
+
|
98
|
+
# We always handle locations as zero based. However, for file links in Markdown we need them to be one based,
|
99
|
+
# which is why instead of the usual subtraction of 1 to line numbers, we are actually adding 1 to columns. The
|
100
|
+
# format for VS Code file URIs is `file:///path/to/file.rb#Lstart_line,start_column-end_line,end_column`
|
101
|
+
uri = URI::Generic.from_path(
|
102
|
+
path: entry.file_path,
|
103
|
+
fragment: "L#{loc.start_line},#{loc.start_column + 1}-#{loc.end_line},#{loc.end_column + 1}",
|
104
|
+
)
|
105
|
+
|
106
|
+
definitions << "[#{entry.file_name}](#{uri})"
|
107
|
+
content << "\n\n#{entry.comments.join("\n")}" unless entry.comments.empty?
|
108
|
+
end
|
89
109
|
|
90
|
-
contents = Interface::MarkupContent.new(
|
91
|
-
|
110
|
+
contents = Interface::MarkupContent.new(
|
111
|
+
kind: "markdown",
|
112
|
+
value: "#{title}\n\n**Definitions**: #{definitions.join(" | ")}\n\n#{content}",
|
113
|
+
)
|
114
|
+
@response = Interface::Hover.new(range: range_from_syntax_tree_node(node), contents: contents)
|
92
115
|
end
|
93
116
|
end
|
94
117
|
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module RubyLsp
|
5
|
+
module Requests
|
6
|
+
# 
|
7
|
+
#
|
8
|
+
# The [workspace symbol](https://microsoft.github.io/language-server-protocol/specification#workspace_symbol)
|
9
|
+
# request allows fuzzy searching declarations in the entire project. On VS Code, use CTRL/CMD + T to search for
|
10
|
+
# symbols.
|
11
|
+
#
|
12
|
+
# # Example
|
13
|
+
#
|
14
|
+
# ```ruby
|
15
|
+
# # Searching for `Floo` will fuzzy match and return all declarations according to the query, including this `Foo`
|
16
|
+
# class
|
17
|
+
# class Foo
|
18
|
+
# end
|
19
|
+
# ```
|
20
|
+
#
|
21
|
+
class WorkspaceSymbol
|
22
|
+
extend T::Sig
|
23
|
+
|
24
|
+
sig { params(query: T.nilable(String), index: RubyIndexer::Index).void }
|
25
|
+
def initialize(query, index)
|
26
|
+
@query = query
|
27
|
+
@index = index
|
28
|
+
end
|
29
|
+
|
30
|
+
sig { returns(T::Array[Interface::WorkspaceSymbol]) }
|
31
|
+
def run
|
32
|
+
bundle_path = begin
|
33
|
+
Bundler.bundle_path.to_s
|
34
|
+
rescue Bundler::GemfileNotFound
|
35
|
+
nil
|
36
|
+
end
|
37
|
+
|
38
|
+
@index.fuzzy_search(@query).filter_map do |entry|
|
39
|
+
# If the project is using Sorbet, we let Sorbet handle symbols defined inside the project itself and RBIs, but
|
40
|
+
# we still return entries defined in gems to allow developers to jump directly to the source
|
41
|
+
file_path = entry.file_path
|
42
|
+
if DependencyDetector::HAS_TYPECHECKER && bundle_path && !file_path.start_with?(bundle_path) &&
|
43
|
+
!file_path.start_with?(RbConfig::CONFIG["rubylibdir"])
|
44
|
+
|
45
|
+
next
|
46
|
+
end
|
47
|
+
|
48
|
+
kind = kind_for_entry(entry)
|
49
|
+
loc = entry.location
|
50
|
+
|
51
|
+
# We use the namespace as the container name, but we also use the full name as the regular name. The reason we
|
52
|
+
# do this is to allow people to search for fully qualified names (e.g.: `Foo::Bar`). If we only included the
|
53
|
+
# short name `Bar`, then searching for `Foo::Bar` would not return any results
|
54
|
+
*container, _short_name = entry.name.split("::")
|
55
|
+
|
56
|
+
Interface::WorkspaceSymbol.new(
|
57
|
+
name: entry.name,
|
58
|
+
container_name: T.must(container).join("::"),
|
59
|
+
kind: kind,
|
60
|
+
location: Interface::Location.new(
|
61
|
+
uri: URI::Generic.from_path(path: file_path).to_s,
|
62
|
+
range: Interface::Range.new(
|
63
|
+
start: Interface::Position.new(line: loc.start_line - 1, character: loc.start_column),
|
64
|
+
end: Interface::Position.new(line: loc.end_line - 1, character: loc.end_column),
|
65
|
+
),
|
66
|
+
),
|
67
|
+
)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
sig { params(entry: RubyIndexer::Index::Entry).returns(T.nilable(Integer)) }
|
74
|
+
def kind_for_entry(entry)
|
75
|
+
case entry
|
76
|
+
when RubyIndexer::Index::Entry::Class
|
77
|
+
Constant::SymbolKind::CLASS
|
78
|
+
when RubyIndexer::Index::Entry::Module
|
79
|
+
Constant::SymbolKind::NAMESPACE
|
80
|
+
when RubyIndexer::Index::Entry::Constant
|
81
|
+
Constant::SymbolKind::CONSTANT
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/lib/ruby_lsp/requests.rb
CHANGED
@@ -41,6 +41,7 @@ module RubyLsp
|
|
41
41
|
autoload :CodeLens, "ruby_lsp/requests/code_lens"
|
42
42
|
autoload :Definition, "ruby_lsp/requests/definition"
|
43
43
|
autoload :ShowSyntaxTree, "ruby_lsp/requests/show_syntax_tree"
|
44
|
+
autoload :WorkspaceSymbol, "ruby_lsp/requests/workspace_symbol"
|
44
45
|
|
45
46
|
# :nodoc:
|
46
47
|
module Support
|
@@ -6,6 +6,7 @@ require "bundler"
|
|
6
6
|
require "fileutils"
|
7
7
|
require "pathname"
|
8
8
|
require "digest"
|
9
|
+
require "time"
|
9
10
|
|
10
11
|
# This file is a script that will configure a custom bundle for the Ruby LSP. The custom bundle allows developers to use
|
11
12
|
# the Ruby LSP without including the gem in their application's Gemfile while at the same time giving us access to the
|
@@ -17,6 +18,8 @@ module RubyLsp
|
|
17
18
|
|
18
19
|
class BundleNotLocked < StandardError; end
|
19
20
|
|
21
|
+
FOUR_HOURS = T.let(4 * 60 * 60, Integer)
|
22
|
+
|
20
23
|
sig { params(project_path: String, branch: T.nilable(String)).void }
|
21
24
|
def initialize(project_path, branch: nil)
|
22
25
|
@project_path = project_path
|
@@ -27,6 +30,7 @@ module RubyLsp
|
|
27
30
|
@custom_gemfile = T.let(@custom_dir + "Gemfile", Pathname)
|
28
31
|
@custom_lockfile = T.let(@custom_dir + "Gemfile.lock", Pathname)
|
29
32
|
@lockfile_hash_path = T.let(@custom_dir + "main_lockfile_hash", Pathname)
|
33
|
+
@last_updated_path = T.let(@custom_dir + "last_updated", Pathname)
|
30
34
|
|
31
35
|
# Regular bundle paths
|
32
36
|
@gemfile = T.let(
|
@@ -173,15 +177,16 @@ module RubyLsp
|
|
173
177
|
# custom `.ruby-lsp/Gemfile.lock` already exists and includes both gems
|
174
178
|
command = +""
|
175
179
|
|
176
|
-
if
|
177
|
-
custom_bundle_dependencies["ruby-lsp"].nil? || custom_bundle_dependencies["debug"].nil?
|
180
|
+
if should_bundle_install?
|
178
181
|
# Install gems using the custom bundle
|
179
|
-
command << "bundle install "
|
182
|
+
command << "bundle check || bundle install "
|
180
183
|
else
|
181
184
|
# If ruby-lsp or debug are not in the Gemfile, try to update them to the latest version
|
182
185
|
command << "bundle update "
|
183
186
|
command << "ruby-lsp " unless @dependencies["ruby-lsp"]
|
184
187
|
command << "debug " unless @dependencies["debug"]
|
188
|
+
|
189
|
+
@last_updated_path.write(Time.now.iso8601)
|
185
190
|
end
|
186
191
|
|
187
192
|
# Redirect stdout to stderr to prevent going into an infinite loop. The extension might confuse stdout output with
|
@@ -193,5 +198,12 @@ module RubyLsp
|
|
193
198
|
system(env, command)
|
194
199
|
[bundle_gemfile.to_s, expanded_path]
|
195
200
|
end
|
201
|
+
|
202
|
+
sig { returns(T::Boolean) }
|
203
|
+
def should_bundle_install?
|
204
|
+
(!@dependencies["ruby-lsp"].nil? && !@dependencies["debug"].nil?) ||
|
205
|
+
custom_bundle_dependencies["ruby-lsp"].nil? || custom_bundle_dependencies["debug"].nil? ||
|
206
|
+
(@last_updated_path.exist? && Time.parse(@last_updated_path.read) > (Time.now - FOUR_HOURS))
|
207
|
+
end
|
196
208
|
end
|
197
209
|
end
|
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.
|
4
|
+
version: 0.9.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shopify
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-08-
|
11
|
+
date: 2023-08-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: language_server-protocol
|
@@ -62,16 +62,22 @@ dependencies:
|
|
62
62
|
name: yarp
|
63
63
|
requirement: !ruby/object:Gem::Requirement
|
64
64
|
requirements:
|
65
|
-
- - "
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0.9'
|
68
|
+
- - "<"
|
66
69
|
- !ruby/object:Gem::Version
|
67
|
-
version: 0.
|
70
|
+
version: '0.10'
|
68
71
|
type: :runtime
|
69
72
|
prerelease: false
|
70
73
|
version_requirements: !ruby/object:Gem::Requirement
|
71
74
|
requirements:
|
72
|
-
- - "
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0.9'
|
78
|
+
- - "<"
|
73
79
|
- !ruby/object:Gem::Version
|
74
|
-
version: 0.
|
80
|
+
version: '0.10'
|
75
81
|
description: An opinionated language server for Ruby
|
76
82
|
email:
|
77
83
|
- ruby@shopify.com
|
@@ -130,7 +136,6 @@ files:
|
|
130
136
|
- lib/ruby_lsp/requests/support/formatter_runner.rb
|
131
137
|
- lib/ruby_lsp/requests/support/highlight_target.rb
|
132
138
|
- lib/ruby_lsp/requests/support/prefix_tree.rb
|
133
|
-
- lib/ruby_lsp/requests/support/rails_document_client.rb
|
134
139
|
- lib/ruby_lsp/requests/support/rubocop_diagnostic.rb
|
135
140
|
- lib/ruby_lsp/requests/support/rubocop_diagnostics_runner.rb
|
136
141
|
- lib/ruby_lsp/requests/support/rubocop_formatting_runner.rb
|
@@ -140,6 +145,7 @@ files:
|
|
140
145
|
- lib/ruby_lsp/requests/support/sorbet.rb
|
141
146
|
- lib/ruby_lsp/requests/support/source_uri.rb
|
142
147
|
- lib/ruby_lsp/requests/support/syntax_tree_formatting_runner.rb
|
148
|
+
- lib/ruby_lsp/requests/workspace_symbol.rb
|
143
149
|
- lib/ruby_lsp/server.rb
|
144
150
|
- lib/ruby_lsp/setup_bundler.rb
|
145
151
|
- lib/ruby_lsp/store.rb
|
@@ -1,122 +0,0 @@
|
|
1
|
-
# typed: strict
|
2
|
-
# frozen_string_literal: true
|
3
|
-
|
4
|
-
require "net/http"
|
5
|
-
|
6
|
-
module RubyLsp
|
7
|
-
module Requests
|
8
|
-
module Support
|
9
|
-
class RailsDocumentClient
|
10
|
-
RAILS_DOC_HOST = "https://api.rubyonrails.org"
|
11
|
-
SUPPORTED_RAILS_DOC_NAMESPACES = T.let(
|
12
|
-
Regexp.union(
|
13
|
-
/ActionDispatch/,
|
14
|
-
/ActionController/,
|
15
|
-
/AbstractController/,
|
16
|
-
/ActiveRecord/,
|
17
|
-
/ActiveModel/,
|
18
|
-
/ActiveStorage/,
|
19
|
-
/ActionText/,
|
20
|
-
/ActiveJob/,
|
21
|
-
).freeze,
|
22
|
-
Regexp,
|
23
|
-
)
|
24
|
-
|
25
|
-
RAILTIES_VERSION = T.let(
|
26
|
-
[*::Gem::Specification.default_stubs, *::Gem::Specification.stubs].find do |s|
|
27
|
-
s.name == "railties"
|
28
|
-
end&.version&.to_s,
|
29
|
-
T.nilable(String),
|
30
|
-
)
|
31
|
-
|
32
|
-
class << self
|
33
|
-
extend T::Sig
|
34
|
-
sig do
|
35
|
-
params(name: String).returns(T::Array[String])
|
36
|
-
end
|
37
|
-
def generate_rails_document_urls(name)
|
38
|
-
docs = search_index&.fetch(name, nil)
|
39
|
-
|
40
|
-
return [] unless docs
|
41
|
-
|
42
|
-
docs.map do |doc|
|
43
|
-
owner = doc[:owner]
|
44
|
-
|
45
|
-
link_name =
|
46
|
-
# class/module name
|
47
|
-
if owner == name
|
48
|
-
name
|
49
|
-
else
|
50
|
-
"#{owner}##{name}"
|
51
|
-
end
|
52
|
-
|
53
|
-
"[Rails Document: `#{link_name}`](#{doc[:url]})"
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
sig { returns(T.nilable(T::Hash[String, T::Array[T::Hash[Symbol, String]]])) }
|
58
|
-
private def search_index
|
59
|
-
@rails_documents ||= T.let(
|
60
|
-
build_search_index,
|
61
|
-
T.nilable(T::Hash[String, T::Array[T::Hash[Symbol, String]]]),
|
62
|
-
)
|
63
|
-
end
|
64
|
-
|
65
|
-
sig { returns(T.nilable(T::Hash[String, T::Array[T::Hash[Symbol, String]]])) }
|
66
|
-
private def build_search_index
|
67
|
-
return unless RAILTIES_VERSION
|
68
|
-
|
69
|
-
warn("Fetching Rails Documents...")
|
70
|
-
|
71
|
-
response = Net::HTTP.get_response(URI("#{RAILS_DOC_HOST}/v#{RAILTIES_VERSION}/js/search_index.js"))
|
72
|
-
|
73
|
-
if response.code == "302"
|
74
|
-
# If the version's doc is not found, e.g. Rails main, it'll be redirected
|
75
|
-
# In this case, we just fetch the latest doc
|
76
|
-
response = Net::HTTP.get_response(URI("#{RAILS_DOC_HOST}/js/search_index.js"))
|
77
|
-
end
|
78
|
-
|
79
|
-
if response.code == "200"
|
80
|
-
process_search_index(response.body)
|
81
|
-
else
|
82
|
-
warn("Response failed: #{response.inspect}")
|
83
|
-
nil
|
84
|
-
end
|
85
|
-
rescue StandardError => e
|
86
|
-
warn("Exception occurred when fetching Rails document index: #{e.inspect}")
|
87
|
-
end
|
88
|
-
|
89
|
-
sig { params(js: String).returns(T::Hash[String, T::Array[T::Hash[Symbol, String]]]) }
|
90
|
-
private def process_search_index(js)
|
91
|
-
raw_data = js.sub("var search_data = ", "")
|
92
|
-
info = JSON.parse(raw_data).dig("index", "info")
|
93
|
-
|
94
|
-
# An entry looks like this:
|
95
|
-
#
|
96
|
-
# ["belongs_to", # method or module/class
|
97
|
-
# "ActiveRecord::Associations::ClassMethods", # method owner
|
98
|
-
# "classes/ActiveRecord/Associations/ClassMethods.html#method-i-belongs_to", # path to the document
|
99
|
-
# "(name, scope = nil, **options)", # method's parameters
|
100
|
-
# "<p>Specifies a one-to-one association with another class..."] # document preview
|
101
|
-
#
|
102
|
-
info.each_with_object({}) do |(method_or_class, method_owner, doc_path, _, doc_preview), table|
|
103
|
-
# If a method doesn't have documentation, there's no need to generate the link to it.
|
104
|
-
next if doc_preview.nil? || doc_preview.empty?
|
105
|
-
|
106
|
-
# If the method or class/module is not from the supported namespace, reject it
|
107
|
-
next unless [method_or_class, method_owner].any? do |elem|
|
108
|
-
elem.match?(SUPPORTED_RAILS_DOC_NAMESPACES)
|
109
|
-
end
|
110
|
-
|
111
|
-
owner = method_owner.empty? ? method_or_class : method_owner
|
112
|
-
table[method_or_class] ||= []
|
113
|
-
# It's possible to have multiple modules defining the same method name. For example,
|
114
|
-
# both `ActiveRecord::FinderMethods` and `ActiveRecord::Associations::CollectionProxy` defines `#find`
|
115
|
-
table[method_or_class] << { owner: owner, url: "#{RAILS_DOC_HOST}/v#{RAILTIES_VERSION}/#{doc_path}" }
|
116
|
-
end
|
117
|
-
end
|
118
|
-
end
|
119
|
-
end
|
120
|
-
end
|
121
|
-
end
|
122
|
-
end
|