ruby-lsp 0.8.1 → 0.9.1
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 +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 +2 -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: '08f4db46a392926873115c77175be859ecdfc4c5093e003f913eec7288f05fac'
|
4
|
+
data.tar.gz: 2f34acc2fc69ab4dd6ad5fc9961a86c020d550284ed18aefd3221b0525af2628
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 99a9e243c0dd2f763532a5a1c52241ac86c9a7ba2ccb0814932d8e08b17add78143566ac26ad609b59ff8d96e1d2844b53995b85b473cb736c5f87e2a30c5a26
|
7
|
+
data.tar.gz: 2a1669be7a054a5d19b0b7eff113347571928cb601e1b432c6f49bc3f349a530b6d0df45c7ea5dfa46add7b69fd347713ec33f95fdca9baa896855f958814209
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.9.1
|
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
|
-
# ![Hover demo](../../
|
6
|
+
# ![Hover demo](../../hover.gif)
|
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
|
+
# ![Workspace symbol demo](../../workspace_symbol.gif)
|
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
@@ -21,6 +21,7 @@ module RubyLsp
|
|
21
21
|
# - [CodeLens](rdoc-ref:RubyLsp::Requests::CodeLens)
|
22
22
|
# - [Definition](rdoc-ref:RubyLsp::Requests::Definition)
|
23
23
|
# - [ShowSyntaxTree](rdoc-ref:RubyLsp::Requests::ShowSyntaxTree)
|
24
|
+
# - [WorkspaceSymbol](rdoc-ref:RubyLsp::Requests::WorkspaceSymbol)
|
24
25
|
|
25
26
|
module Requests
|
26
27
|
autoload :BaseRequest, "ruby_lsp/requests/base_request"
|
@@ -41,6 +42,7 @@ module RubyLsp
|
|
41
42
|
autoload :CodeLens, "ruby_lsp/requests/code_lens"
|
42
43
|
autoload :Definition, "ruby_lsp/requests/definition"
|
43
44
|
autoload :ShowSyntaxTree, "ruby_lsp/requests/show_syntax_tree"
|
45
|
+
autoload :WorkspaceSymbol, "ruby_lsp/requests/workspace_symbol"
|
44
46
|
|
45
47
|
# :nodoc:
|
46
48
|
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.1
|
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
|