Skip to content

Commit 71b31db

Browse files
authored
Use indexed paths to provide jump to require definition (#975)
Now that we have all require paths indexed, we do not need to search the $LOAD_PATH to find the location of a require. We can just access the index and jump directly
1 parent 4888caa commit 71b31db

File tree

8 files changed

+70
-35
lines changed

8 files changed

+70
-35
lines changed

lib/ruby_indexer/lib/ruby_indexer/index.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def initialize
2828
@files_to_entries = T.let({}, T::Hash[String, T::Array[Entry]])
2929

3030
# Holds all require paths for every indexed item so that we can provide autocomplete for requires
31-
@require_paths_tree = T.let(PrefixTree[String].new, PrefixTree[String])
31+
@require_paths_tree = T.let(PrefixTree[IndexablePath].new, PrefixTree[IndexablePath])
3232
end
3333

3434
sig { params(indexable: IndexablePath).void }
@@ -73,7 +73,7 @@ def [](fully_qualified_name)
7373
@entries[fully_qualified_name.delete_prefix("::")]
7474
end
7575

76-
sig { params(query: String).returns(T::Array[String]) }
76+
sig { params(query: String).returns(T::Array[IndexablePath]) }
7777
def search_require_paths(query)
7878
@require_paths_tree.search(query)
7979
end
@@ -147,7 +147,7 @@ def index_single(indexable_path, source = nil)
147147
visitor.run
148148

149149
require_path = indexable_path.require_path
150-
@require_paths_tree.insert(require_path, require_path) if require_path
150+
@require_paths_tree.insert(require_path, indexable_path) if require_path
151151
rescue Errno::EISDIR
152152
# If `path` is a directory, just ignore it and continue indexing
153153
end

lib/ruby_indexer/test/index_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ class Foo
136136
end
137137
RUBY
138138

139-
assert_equal(["path/foo", "path/other_foo"], @index.search_require_paths("path"))
139+
assert_equal(["path/foo", "path/other_foo"], @index.search_require_paths("path").map(&:require_path))
140140
end
141141

142142
def test_searching_for_entries_based_on_prefix

lib/ruby_lsp/requests/definition.rb

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,15 @@ def on_command(node)
6767
string = argument.parts.first
6868
return unless string.is_a?(SyntaxTree::TStringContent)
6969

70-
required_file = "#{string.value}.rb"
71-
7270
case message
7371
when "require"
74-
candidate = find_file_in_load_path(required_file)
72+
entry = @index.search_require_paths(string.value).find do |indexable_path|
73+
indexable_path.require_path == string.value
74+
end
75+
76+
if entry
77+
candidate = entry.full_path
7578

76-
if candidate
7779
@response = Interface::Location.new(
7880
uri: URI::Generic.from_path(path: candidate).to_s,
7981
range: Interface::Range.new(
@@ -83,19 +85,18 @@ def on_command(node)
8385
)
8486
end
8587
when "require_relative"
88+
required_file = "#{string.value}.rb"
8689
path = @uri.to_standardized_path
8790
current_folder = path ? Pathname.new(CGI.unescape(path)).dirname : Dir.pwd
8891
candidate = File.expand_path(File.join(current_folder, required_file))
8992

90-
if candidate
91-
@response = Interface::Location.new(
92-
uri: URI::Generic.from_path(path: candidate).to_s,
93-
range: Interface::Range.new(
94-
start: Interface::Position.new(line: 0, character: 0),
95-
end: Interface::Position.new(line: 0, character: 0),
96-
),
97-
)
98-
end
93+
@response = Interface::Location.new(
94+
uri: URI::Generic.from_path(path: candidate).to_s,
95+
range: Interface::Range.new(
96+
start: Interface::Position.new(line: 0, character: 0),
97+
end: Interface::Position.new(line: 0, character: 0),
98+
),
99+
)
99100
end
100101
end
101102

@@ -133,18 +134,6 @@ def find_in_index(value)
133134
)
134135
end
135136
end
136-
137-
sig { params(file: String).returns(T.nilable(String)) }
138-
def find_file_in_load_path(file)
139-
return unless file.include?("/")
140-
141-
$LOAD_PATH.each do |p|
142-
found = Dir.glob("**/#{file}", base: p).first
143-
return "#{p}/#{found}" if found
144-
end
145-
146-
nil
147-
end
148137
end
149138
end
150139
end

lib/ruby_lsp/requests/path_completion.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ def initialize(index, emitter, message_queue)
3333

3434
sig { params(node: SyntaxTree::TStringContent).void }
3535
def on_tstring_content(node)
36-
@index.search_require_paths(node.value).sort!.each do |path|
37-
@response << build_completion(path, node)
36+
@index.search_require_paths(node.value).map!(&:require_path).sort!.each do |path|
37+
@response << build_completion(T.must(path), node)
3838
end
3939
end
4040

test/expectations/definition/requires.exp.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"result": {
3-
"uri": "file:///ruby_lsp/utils.rb",
3+
"uri": "file:///ruby_lsp/event_emitter.rb",
44
"range": {
55
"start": {
66
"line": 0,

test/fixtures/requires.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
require "ruby_lsp/utils"
1+
require "ruby_lsp/event_emitter"

test/integration_test.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,13 @@ def test_definition
105105
initialize_lsp(["definition"])
106106
open_file_with("require 'ruby_lsp/utils'")
107107

108-
assert_telemetry("textDocument/didOpen")
108+
read_response("textDocument/didOpen")
109+
110+
# Populate the index
111+
send_request("initialized")
112+
read_response("initialized")
113+
# Read the response for the progress end notification
114+
read_response("$/progress")
109115

110116
response = make_request(
111117
"textDocument/definition",

test/requests/definition_expectations_test.rb

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def run_expectations(source)
1818
index = executor.instance_variable_get(:@index)
1919
index.index_single(
2020
RubyIndexer::IndexablePath.new(
21-
nil,
21+
"#{Dir.pwd}/lib",
2222
File.expand_path(
2323
"../../test/fixtures/class_reference_target.rb",
2424
__dir__,
@@ -34,6 +34,15 @@ def run_expectations(source)
3434
),
3535
),
3636
)
37+
index.index_single(
38+
RubyIndexer::IndexablePath.new(
39+
"#{Dir.pwd}/lib",
40+
File.expand_path(
41+
"../../lib/ruby_lsp/event_emitter.rb",
42+
__dir__,
43+
),
44+
),
45+
)
3746

3847
begin
3948
# We need to pretend that Sorbet is not a dependency or else we can't properly test
@@ -93,4 +102,35 @@ def test_jumping_to_default_gems
93102
ensure
94103
T.must(message_queue).close
95104
end
105+
106+
def test_jumping_to_default_require_of_a_gem
107+
message_queue = Thread::Queue.new
108+
109+
store = RubyLsp::Store.new
110+
store.set(uri: URI("file:///folder/fake.rb"), source: <<~RUBY, version: 1)
111+
require "bundler"
112+
RUBY
113+
114+
executor = RubyLsp::Executor.new(store, message_queue)
115+
116+
uri = URI::Generic.from_path(path: "#{RbConfig::CONFIG["rubylibdir"]}/bundler.rb")
117+
executor.instance_variable_get(:@index).index_single(
118+
RubyIndexer::IndexablePath.new(RbConfig::CONFIG["rubylibdir"], T.must(uri.to_standardized_path)),
119+
)
120+
121+
Dir.glob("#{RbConfig::CONFIG["rubylibdir"]}/bundler/*.rb").each do |path|
122+
executor.instance_variable_get(:@index).index_single(
123+
RubyIndexer::IndexablePath.new(RbConfig::CONFIG["rubylibdir"], path),
124+
)
125+
end
126+
127+
response = executor.execute({
128+
method: "textDocument/definition",
129+
params: { textDocument: { uri: "file:///folder/fake.rb" }, position: { character: 10, line: 0 } },
130+
}).response
131+
132+
assert_equal(uri.to_s, response.attributes[:uri])
133+
ensure
134+
T.must(message_queue).close
135+
end
96136
end

0 commit comments

Comments
 (0)