diff --git a/exe/ruby-lsp b/exe/ruby-lsp index dd318326e..9e551f471 100755 --- a/exe/ruby-lsp +++ b/exe/ruby-lsp @@ -128,9 +128,9 @@ if options[:doctor] puts "Globbing for indexable files" - index.configuration.indexables.each do |uri| - puts "indexing: #{uri}" - index.index_single(uri) + index.configuration.indexables.each do |indexable| + puts "indexing: #{indexable.full_path}" + index.index_single(indexable) end return end diff --git a/exe/ruby-lsp-check b/exe/ruby-lsp-check index 0b3f71f5e..ad2d3f6b3 100755 --- a/exe/ruby-lsp-check +++ b/exe/ruby-lsp-check @@ -46,10 +46,10 @@ puts "Verifying that indexing executes successfully. This may take a while..." index = RubyIndexer::Index.new indexables = index.configuration.indexables -indexables.each_with_index do |uri, i| - index.index_single(uri) +indexables.each_with_index do |indexable, i| + index.index_single(indexable) rescue => e - errors[uri.to_standarized_path] = e + errors[indexable.full_path] = e ensure print("\033[M\033[0KIndexed #{i + 1}/#{indexables.length}") unless ENV["CI"] end diff --git a/lib/ruby_indexer/lib/ruby_indexer/configuration.rb b/lib/ruby_indexer/lib/ruby_indexer/configuration.rb index ff0cb500c..330c64b70 100644 --- a/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +++ b/lib/ruby_indexer/lib/ruby_indexer/configuration.rb @@ -52,7 +52,7 @@ def initialize ) end - sig { returns(T::Array[FileUri]) } + sig { returns(T::Array[IndexablePath]) } def indexables excluded_gems = @excluded_gems - @included_gems locked_gems = Bundler.locked_gems&.specs @@ -74,7 +74,7 @@ def indexables load_path_entry = $LOAD_PATH.find { |load_path| path.start_with?(load_path) } end - ResourceUri.file(path, load_path_entry) + IndexablePath.new(load_path_entry, path) end end @@ -91,7 +91,7 @@ def indexables # Remove user specified patterns indexables.reject! do |indexable| excluded_patterns.any? do |pattern| - File.fnmatch?(pattern, indexable.to_standardized_path, File::FNM_PATHNAME | File::FNM_EXTGLOB) + File.fnmatch?(pattern, indexable.full_path, File::FNM_PATHNAME | File::FNM_EXTGLOB) end end @@ -123,12 +123,12 @@ def indexables # If the default_path is a directory, we index all the Ruby files in it indexables.concat( Dir.glob(File.join(default_path, "**", "*.rb"), File::FNM_PATHNAME | File::FNM_EXTGLOB).map! do |path| - ResourceUri.file(path, RbConfig::CONFIG["rubylibdir"]) + IndexablePath.new(RbConfig::CONFIG["rubylibdir"], path) end, ) elsif pathname.extname == ".rb" # If the default_path is a Ruby file, we index it - indexables << ResourceUri.file(default_path, RbConfig::CONFIG["rubylibdir"]) + indexables << IndexablePath.new(RbConfig::CONFIG["rubylibdir"], default_path) end end @@ -146,7 +146,7 @@ def indexables indexables.concat( spec.require_paths.flat_map do |require_path| load_path_entry = File.join(spec.full_gem_path, require_path) - Dir.glob(File.join(load_path_entry, "**", "*.rb")).map! { |path| ResourceUri.file(path, load_path_entry) } + Dir.glob(File.join(load_path_entry, "**", "*.rb")).map! { |path| IndexablePath.new(load_path_entry, path) } end, ) rescue Gem::MissingSpecError @@ -155,7 +155,7 @@ def indexables # just ignore if they're missing end - indexables.uniq!(&:to_s) + indexables.uniq!(&:full_path) indexables end diff --git a/lib/ruby_indexer/lib/ruby_indexer/index.rb b/lib/ruby_indexer/lib/ruby_indexer/index.rb index 6741c1d14..dc2353408 100644 --- a/lib/ruby_indexer/lib/ruby_indexer/index.rb +++ b/lib/ruby_indexer/lib/ruby_indexer/index.rb @@ -34,7 +34,7 @@ def initialize @files_to_entries = T.let({}, T::Hash[String, T::Array[Entry]]) # Holds all require paths for every indexed item so that we can provide autocomplete for requires - @require_paths_tree = T.let(PrefixTree[ResourceUri].new, PrefixTree[ResourceUri]) + @require_paths_tree = T.let(PrefixTree[IndexablePath].new, PrefixTree[IndexablePath]) # Holds the linearized ancestors list for every namespace @ancestors = T.let({}, T::Hash[String, T::Array[String]]) @@ -63,35 +63,31 @@ def register_included_hook(module_name, &hook) (@included_hooks[module_name] ||= []) << hook end - sig { params(uri: ResourceUri).void } - def delete(uri) - path = uri.to_standardized_path - - if path - # For each constant discovered in `path`, delete the associated entry from the index. If there are no entries - # left, delete the constant from the index. - @files_to_entries[path]&.each do |entry| - name = entry.name - entries = @entries[name] - next unless entries - - # Delete the specific entry from the list for this name - entries.delete(entry) - - # If all entries were deleted, then remove the name from the hash and from the prefix tree. Otherwise, update - # the prefix tree with the current entries - if entries.empty? - @entries.delete(name) - @entries_tree.delete(name) - else - @entries_tree.insert(name, entries) - end - end + sig { params(indexable: IndexablePath).void } + def delete(indexable) + # For each constant discovered in `path`, delete the associated entry from the index. If there are no entries + # left, delete the constant from the index. + @files_to_entries[indexable.full_path]&.each do |entry| + name = entry.name + entries = @entries[name] + next unless entries - @files_to_entries.delete(path) + # Delete the specific entry from the list for this name + entries.delete(entry) + + # If all entries were deleted, then remove the name from the hash and from the prefix tree. Otherwise, update + # the prefix tree with the current entries + if entries.empty? + @entries.delete(name) + @entries_tree.delete(name) + else + @entries_tree.insert(name, entries) + end end - require_path = uri.require_path + @files_to_entries.delete(indexable.full_path) + + require_path = indexable.require_path @require_paths_tree.delete(require_path) if require_path end @@ -109,7 +105,7 @@ def [](fully_qualified_name) @entries[fully_qualified_name.delete_prefix("::")] end - sig { params(query: String).returns(T::Array[ResourceUri]) } + sig { params(query: String).returns(T::Array[IndexablePath]) } def search_require_paths(query) @require_paths_tree.search(query) end @@ -296,37 +292,33 @@ def resolve(name, nesting, seen_names = []) nil end - # Index all files for the given uris, which defaults to what is configured. A block can be used to track and control - # indexing progress. That block is invoked with the current progress percentage and should return `true` to continue - # indexing or `false` to stop indexing. + # Index all files for the given indexable paths, which defaults to what is configured. A block can be used to track + # and control indexing progress. That block is invoked with the current progress percentage and should return `true` + # to continue indexing or `false` to stop indexing. sig do params( - uris: T::Array[ResourceUri], + indexable_paths: T::Array[IndexablePath], block: T.nilable(T.proc.params(progress: Integer).returns(T::Boolean)), ).void end - def index_all(uris: @configuration.indexables, &block) + def index_all(indexable_paths: @configuration.indexables, &block) RBSIndexer.new(self).index_ruby_core # Calculate how many paths are worth 1% of progress - progress_step = (uris.length / 100.0).ceil + progress_step = (indexable_paths.length / 100.0).ceil - uris.each_with_index do |uri, index| + indexable_paths.each_with_index do |path, index| if block && index % progress_step == 0 progress = (index / progress_step) + 1 break unless block.call(progress) end - index_single(uri) + index_single(path) end end - sig { params(uri: ResourceUri, source: T.nilable(String)).void } - def index_single(uri, source = nil) - path = uri.to_standardized_path - # Remove once we support indexing non file URIs - return unless path - - content = source || File.read(path) + sig { params(indexable_path: IndexablePath, source: T.nilable(String)).void } + def index_single(indexable_path, source = nil) + content = source || File.read(indexable_path.full_path) dispatcher = Prism::Dispatcher.new result = Prism.parse(content) @@ -334,15 +326,15 @@ def index_single(uri, source = nil) self, dispatcher, result, - path, + indexable_path.full_path, enhancements: @enhancements, ) dispatcher.dispatch(result.value) indexing_errors = listener.indexing_errors.uniq - require_path = uri.require_path - @require_paths_tree.insert(require_path, uri) if require_path + require_path = indexable_path.require_path + @require_paths_tree.insert(require_path, indexable_path) if require_path if indexing_errors.any? indexing_errors.each do |error| @@ -354,7 +346,7 @@ def index_single(uri, source = nil) # it rescue SystemStackError => e if e.backtrace&.first&.include?("prism") - $stderr.puts "Prism error indexing #{uri}: #{e.message}" + $stderr.puts "Prism error indexing #{indexable_path.full_path}: #{e.message}" else raise end @@ -551,17 +543,16 @@ def instance_variable_completion_candidates(name, owner_name) variables end - # Synchronizes a change made to the given uri. This method will ensure that new declarations are indexed, removed - # declarations removed and that the ancestor linearization cache is cleared if necessary - sig { params(uri: ResourceUri).void } - def handle_change(uri) - path = T.must(uri.to_standardized_path) - original_entries = @files_to_entries[path] + # Synchronizes a change made to the given indexable path. This method will ensure that new declarations are indexed, + # removed declarations removed and that the ancestor linearization cache is cleared if necessary + sig { params(indexable: IndexablePath).void } + def handle_change(indexable) + original_entries = @files_to_entries[indexable.full_path] - delete(uri) - index_single(uri) + delete(indexable) + index_single(indexable) - updated_entries = @files_to_entries[path] + updated_entries = @files_to_entries[indexable.full_path] return unless original_entries && updated_entries diff --git a/lib/ruby_indexer/lib/ruby_indexer/indexable_path.rb b/lib/ruby_indexer/lib/ruby_indexer/indexable_path.rb new file mode 100644 index 000000000..2d2b1d585 --- /dev/null +++ b/lib/ruby_indexer/lib/ruby_indexer/indexable_path.rb @@ -0,0 +1,29 @@ +# typed: strict +# frozen_string_literal: true + +module RubyIndexer + class IndexablePath + extend T::Sig + + sig { returns(T.nilable(String)) } + attr_reader :require_path + + sig { returns(String) } + attr_reader :full_path + + # An IndexablePath is instantiated with a load_path_entry and a full_path. The load_path_entry is where the file can + # be found in the $LOAD_PATH, which we use to determine the require_path. The load_path_entry may be `nil` if the + # indexer is configured to go through files that do not belong in the $LOAD_PATH. For example, + # `sorbet/tapioca/require.rb` ends up being a part of the paths to be indexed because it's a Ruby file inside the + # project, but the `sorbet` folder is not a part of the $LOAD_PATH. That means that both its load_path_entry and + # require_path will be `nil`, since it cannot be required by the project + sig { params(load_path_entry: T.nilable(String), full_path: String).void } + def initialize(load_path_entry, full_path) + @full_path = full_path + @require_path = T.let( + load_path_entry ? full_path.delete_prefix("#{load_path_entry}/").delete_suffix(".rb") : nil, + T.nilable(String), + ) + end + end +end diff --git a/lib/ruby_indexer/lib/ruby_indexer/resource_uri.rb b/lib/ruby_indexer/lib/ruby_indexer/resource_uri.rb deleted file mode 100644 index 7f6813e8d..000000000 --- a/lib/ruby_indexer/lib/ruby_indexer/resource_uri.rb +++ /dev/null @@ -1,105 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -module RubyIndexer - # The indexer ResourceUri class is a specialization of the regular URI class. It includes convenience methods, special - # handling to support Windows paths and simplifications for URI elements that are not used in an LSP context. - # - # For example, it does not do anything with the `host` portion of the URI, but we include a `require_path` attribute, - # so that we know how this URI is mapped in the `$LOAD_PATH` if applicable - class ResourceUri < URI::Generic - class << self - extend T::Sig - - sig { params(path: String, load_path_entry: T.nilable(String)).returns(FileUri) } - def file(path, load_path_entry = nil) - require_path = if load_path_entry - rp = path.delete_prefix("#{load_path_entry}/") - rp.delete_suffix!(".rb") - rp - end - - FileUri.new(path: path, require_path: require_path) - end - end - - extend T::Sig - - sig { returns(T.nilable(String)) } - attr_reader :require_path - - sig do - params( - scheme: T.nilable(String), - path: T.nilable(String), - opaque: T.nilable(String), - fragment: T.nilable(String), - require_path: T.nilable(String), - ).void - end - def initialize(scheme: "file", path: nil, opaque: nil, fragment: nil, require_path: nil) - # On Windows, if the path begins with the disk name, we need to add a leading slash to make it a valid URI - escaped_path = if !path - nil - elsif /^[A-Z]:/i.match?(path) - URI::DEFAULT_PARSER.escape("/#{path}") - elsif path.start_with?("//?/") - # Some paths on Windows start with "//?/". This is a special prefix that allows for long file paths - URI::DEFAULT_PARSER.escape(path.delete_prefix("//?")) - else - URI::DEFAULT_PARSER.escape(path) - end - - # scheme, userinfo, host, port, registry, path, opaque, query, fragment, parser, arg_check - super(scheme, nil, nil, nil, nil, escaped_path, opaque, nil, fragment, URI::DEFAULT_PARSER, true) - @require_path = require_path - end - - sig { returns(String) } - def to_s - "#{@scheme}://#{@path || @opaque}" - end - - sig { returns(String) } - def to_s_with_fragment - str = to_s - return str unless @fragment - - "#{str}##{@fragment}" - end - - sig { returns(T.nilable(String)) } - def to_standardized_path - parsed_path = @path - return unless parsed_path - - unescaped_path = URI::DEFAULT_PARSER.unescape(parsed_path) - - # On Windows, when we're getting the file system path back from the URI, we need to remove the leading forward - # slash - if %r{^/[A-Z]:}i.match?(unescaped_path) - unescaped_path.delete_prefix("/") - else - unescaped_path - end - end - end - - class FileUri < ResourceUri - extend T::Sig - - sig { override.returns(String) } - def to_standardized_path - parsed_path = T.must(@path) - unescaped_path = URI::DEFAULT_PARSER.unescape(parsed_path) - - # On Windows, when we're getting the file system path back from the URI, we need to remove the leading forward - # slash - if %r{^/[A-Z]:}i.match?(unescaped_path) - unescaped_path.delete_prefix("/") - else - unescaped_path - end - end - end -end diff --git a/lib/ruby_indexer/ruby_indexer.rb b/lib/ruby_indexer/ruby_indexer.rb index 8b0994e9d..996af2f8c 100644 --- a/lib/ruby_indexer/ruby_indexer.rb +++ b/lib/ruby_indexer/ruby_indexer.rb @@ -4,7 +4,7 @@ require "yaml" require "did_you_mean" -require "ruby_indexer/lib/ruby_indexer/resource_uri" +require "ruby_indexer/lib/ruby_indexer/indexable_path" require "ruby_indexer/lib/ruby_indexer/declaration_listener" require "ruby_indexer/lib/ruby_indexer/enhancement" require "ruby_indexer/lib/ruby_indexer/index" diff --git a/lib/ruby_indexer/test/classes_and_modules_test.rb b/lib/ruby_indexer/test/classes_and_modules_test.rb index a9e176e95..eae6d6a45 100644 --- a/lib/ruby_indexer/test/classes_and_modules_test.rb +++ b/lib/ruby_indexer/test/classes_and_modules_test.rb @@ -189,7 +189,7 @@ class Foo assert_entry("Foo", Entry::Class, "/fake/path/foo.rb:0-0:1-3") - @index.delete(ResourceUri.new(path: "/fake/path/foo.rb")) + @index.delete(IndexablePath.new(nil, "/fake/path/foo.rb")) refute_entry("Foo") assert_no_indexed_entries diff --git a/lib/ruby_indexer/test/configuration_test.rb b/lib/ruby_indexer/test/configuration_test.rb index 1c76d68e8..2078571c0 100644 --- a/lib/ruby_indexer/test/configuration_test.rb +++ b/lib/ruby_indexer/test/configuration_test.rb @@ -15,11 +15,11 @@ def test_load_configuration_executes_configure_block @config.apply_config({ "excluded_patterns" => ["**/fixtures/**/*.rb"] }) indexables = @config.indexables - assert(indexables.none? { |indexable| indexable.to_s.include?("test/fixtures") }) - assert(indexables.none? { |indexable| indexable.to_s.include?("minitest-reporters") }) - assert(indexables.none? { |indexable| indexable.to_s.include?("ansi") }) - assert(indexables.any? { |indexable| indexable.to_s.include?("sorbet-runtime") }) - assert(indexables.none? { |indexable| indexable.to_s == __FILE__ }) + assert(indexables.none? { |indexable| indexable.full_path.include?("test/fixtures") }) + assert(indexables.none? { |indexable| indexable.full_path.include?("minitest-reporters") }) + assert(indexables.none? { |indexable| indexable.full_path.include?("ansi") }) + assert(indexables.any? { |indexable| indexable.full_path.include?("sorbet-runtime") }) + assert(indexables.none? { |indexable| indexable.full_path == __FILE__ }) end def test_indexables_have_expanded_full_paths @@ -27,7 +27,7 @@ def test_indexables_have_expanded_full_paths indexables = @config.indexables # All paths should be expanded - assert(indexables.all? { |indexable| File.absolute_path?(T.must(indexable.to_standardized_path)) }) + assert(indexables.all? { |indexable| File.absolute_path?(indexable.full_path) }) end def test_indexables_only_includes_gem_require_paths @@ -37,9 +37,7 @@ def test_indexables_only_includes_gem_require_paths next if lazy_spec.name == "ruby-lsp" spec = Gem::Specification.find_by_name(lazy_spec.name) - assert(indexables.none? do |indexable| - indexable.to_standardized_path.start_with?("#{spec.full_gem_path}/test/") - end) + assert(indexables.none? { |indexable| indexable.full_path.start_with?("#{spec.full_gem_path}/test/") }) rescue Gem::MissingSpecError # Transitive dependencies might be missing when running tests on Windows end @@ -49,14 +47,12 @@ def test_indexables_does_not_include_default_gem_path_when_in_bundle indexables = @config.indexables assert( - indexables.none? do |indexable| - indexable.to_standardized_path.start_with?("#{RbConfig::CONFIG["rubylibdir"]}/psych") - end, + indexables.none? { |indexable| indexable.full_path.start_with?("#{RbConfig::CONFIG["rubylibdir"]}/psych") }, ) end def test_indexables_includes_default_gems - indexables = @config.indexables.map(&:to_standardized_path) + indexables = @config.indexables.map(&:full_path) assert_includes(indexables, "#{RbConfig::CONFIG["rubylibdir"]}/pathname.rb") assert_includes(indexables, "#{RbConfig::CONFIG["rubylibdir"]}/ipaddr.rb") @@ -64,7 +60,7 @@ def test_indexables_includes_default_gems end def test_indexables_includes_project_files - indexables = @config.indexables.map(&:to_standardized_path) + indexables = @config.indexables.map(&:full_path) Dir.glob("#{Dir.pwd}/lib/**/*.rb").each do |path| next if path.end_with?("_test.rb") @@ -84,13 +80,13 @@ def test_indexables_avoids_duplicates_if_bundle_path_is_inside_project def test_indexables_does_not_include_gems_own_installed_files indexables = @config.indexables indexables_inside_bundled_lsp = indexables.select do |indexable| - indexable.to_standardized_path.start_with?(Bundler.bundle_path.join("gems", "ruby-lsp").to_s) + indexable.full_path.start_with?(Bundler.bundle_path.join("gems", "ruby-lsp").to_s) end assert_empty( indexables_inside_bundled_lsp, "Indexables should not include files from the gem currently being worked on. " \ - "Included: #{indexables_inside_bundled_lsp.map(&:to_standardized_path)}", + "Included: #{indexables_inside_bundled_lsp.map(&:full_path)}", ) end @@ -99,7 +95,7 @@ def test_indexables_does_not_include_non_ruby_files_inside_rubylibdir FileUtils.touch(path) indexables = @config.indexables - assert(indexables.none? { |indexable| indexable.to_standardized_path == path }) + assert(indexables.none? { |indexable| indexable.full_path == path }) ensure FileUtils.rm(T.must(path)) end @@ -147,7 +143,7 @@ def test_indexables_respect_given_workspace_path @config.workspace_path = dir indexables = @config.indexables - assert(indexables.none? { |indexable| indexable.to_standardized_path.start_with?(File.join(dir, "ignore")) }) + assert(indexables.none? { |indexable| indexable.full_path.start_with?(File.join(dir, "ignore")) }) # After switching the workspace path, all indexables will be found in one of these places: # - The new workspace path @@ -156,10 +152,10 @@ def test_indexables_respect_given_workspace_path # - Default gems assert( indexables.all? do |i| - i.to_standardized_path.start_with?(dir) || - i.to_standardized_path.start_with?(File.join(Dir.pwd, "lib")) || - i.to_standardized_path.start_with?(Bundler.bundle_path.to_s) || - i.to_standardized_path.start_with?(RbConfig::CONFIG["rubylibdir"]) + i.full_path.start_with?(dir) || + i.full_path.start_with?(File.join(Dir.pwd, "lib")) || + i.full_path.start_with?(Bundler.bundle_path.to_s) || + i.full_path.start_with?(RbConfig::CONFIG["rubylibdir"]) end, ) end diff --git a/lib/ruby_indexer/test/index_test.rb b/lib/ruby_indexer/test/index_test.rb index cb158848e..7e1f5a111 100644 --- a/lib/ruby_indexer/test/index_test.rb +++ b/lib/ruby_indexer/test/index_test.rb @@ -6,11 +6,11 @@ module RubyIndexer class IndexTest < TestCase def test_deleting_one_entry_for_a_class - @index.index_single(ResourceUri.new(path: "/fake/path/foo.rb"), <<~RUBY) + @index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY) class Foo end RUBY - @index.index_single(ResourceUri.new(path: "/fake/path/other_foo.rb"), <<~RUBY) + @index.index_single(IndexablePath.new(nil, "/fake/path/other_foo.rb"), <<~RUBY) class Foo end RUBY @@ -18,13 +18,13 @@ class Foo entries = @index["Foo"] assert_equal(2, entries.length) - @index.delete(ResourceUri.new(path: "/fake/path/other_foo.rb")) + @index.delete(IndexablePath.new(nil, "/fake/path/other_foo.rb")) entries = @index["Foo"] assert_equal(1, entries.length) end def test_deleting_all_entries_for_a_class - @index.index_single(ResourceUri.new(path: "/fake/path/foo.rb"), <<~RUBY) + @index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY) class Foo end RUBY @@ -32,13 +32,13 @@ class Foo entries = @index["Foo"] assert_equal(1, entries.length) - @index.delete(ResourceUri.new(path: "/fake/path/foo.rb")) + @index.delete(IndexablePath.new(nil, "/fake/path/foo.rb")) entries = @index["Foo"] assert_nil(entries) end def test_index_resolve - @index.index_single(ResourceUri.new(path: "/fake/path/foo.rb"), <<~RUBY) + @index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY) class Bar; end module Foo @@ -72,7 +72,7 @@ class Something end def test_accessing_with_colon_colon_prefix - @index.index_single(ResourceUri.new(path: "/fake/path/foo.rb"), <<~RUBY) + @index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY) class Bar; end module Foo @@ -92,7 +92,7 @@ class Something end def test_fuzzy_search - @index.index_single(ResourceUri.new(path: "/fake/path/foo.rb"), <<~RUBY) + @index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY) class Zws; end module Qtl @@ -121,17 +121,17 @@ class Something def test_index_single_ignores_directories FileUtils.mkdir("lib/this_is_a_dir.rb") - @index.index_single(ResourceUri.new(path: File.expand_path("lib/this_is_a_dir.rb"))) + @index.index_single(IndexablePath.new(nil, "lib/this_is_a_dir.rb")) ensure FileUtils.rm_r("lib/this_is_a_dir.rb") end def test_searching_for_require_paths - @index.index_single(ResourceUri.new(path: "/fake/path/foo.rb", require_path: "path/foo"), <<~RUBY) + @index.index_single(IndexablePath.new("/fake", "/fake/path/foo.rb"), <<~RUBY) class Foo end RUBY - @index.index_single(ResourceUri.new(path: "/fake/path/other_foo.rb", require_path: "path/other_foo"), <<~RUBY) + @index.index_single(IndexablePath.new("/fake", "/fake/path/other_foo.rb"), <<~RUBY) class Foo end RUBY @@ -140,11 +140,11 @@ class Foo end def test_searching_for_entries_based_on_prefix - @index.index_single(ResourceUri.new(path: "/fake/path/foo.rb", require_path: "path/foo"), <<~RUBY) + @index.index_single(IndexablePath.new("/fake", "/fake/path/foo.rb"), <<~RUBY) class Foo::Bizw end RUBY - @index.index_single(ResourceUri.new(path: "/fake/path/other_foo.rb", require_path: "path/other_foo"), <<~RUBY) + @index.index_single(IndexablePath.new("/fake", "/fake/path/other_foo.rb"), <<~RUBY) class Foo::Bizw end @@ -160,7 +160,7 @@ class Foo::Bizt end def test_resolve_normalizes_top_level_names - @index.index_single(ResourceUri.new(path: "/fake/path/foo.rb", require_path: "path/foo"), <<~RUBY) + @index.index_single(IndexablePath.new("/fake", "/fake/path/foo.rb"), <<~RUBY) class Bar; end module Foo @@ -180,7 +180,7 @@ class Bar; end end def test_resolving_aliases_to_non_existing_constants_with_conflicting_names - @index.index_single(ResourceUri.new(require_path: "path/foo", path: "/fake/path/foo.rb"), <<~RUBY) + @index.index_single(IndexablePath.new("/fake", "/fake/path/foo.rb"), <<~RUBY) class Bar end @@ -346,15 +346,15 @@ def test_indexing_prism_fixtures_succeeds fixtures = Dir.glob("test/fixtures/prism/test/prism/fixtures/**/*.txt") fixtures.each do |fixture| - uri = ResourceUri.new(path: File.expand_path(fixture)) - @index.index_single(uri) + indexable_path = IndexablePath.new("", fixture) + @index.index_single(indexable_path) end refute_empty(@index) end def test_index_single_does_not_fail_for_non_existing_file - @index.index_single(ResourceUri.new(path: "/fake/path/foo.rb")) + @index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb")) entries_after_indexing = @index.names assert_equal(@default_indexed_entries.keys, entries_after_indexing) end @@ -782,8 +782,8 @@ class Bar end RUBY - uri = ResourceUri.new(path: File.join(dir, "foo.rb")) - @index.index_single(uri) + indexable_path = IndexablePath.new(nil, File.join(dir, "foo.rb")) + @index.index_single(indexable_path) assert_equal(["Bar", "Foo", "Object", "Kernel", "BasicObject"], @index.linearized_ancestors_of("Bar")) @@ -796,7 +796,7 @@ class Bar end RUBY - @index.handle_change(uri) + @index.handle_change(indexable_path) assert_empty(@index.instance_variable_get(:@ancestors)) assert_equal(["Bar", "Object", "Kernel", "BasicObject"], @index.linearized_ancestors_of("Bar")) end @@ -816,8 +816,8 @@ class Bar end RUBY - uri = ResourceUri.new(path: File.join(dir, "foo.rb")) - @index.index_single(uri) + indexable_path = IndexablePath.new(nil, File.join(dir, "foo.rb")) + @index.index_single(indexable_path) assert_equal(["Bar", "Foo", "Object", "Kernel", "BasicObject"], @index.linearized_ancestors_of("Bar")) @@ -833,7 +833,7 @@ def baz; end end RUBY - @index.handle_change(uri) + @index.handle_change(indexable_path) refute_empty(@index.instance_variable_get(:@ancestors)) assert_equal(["Bar", "Foo", "Object", "Kernel", "BasicObject"], @index.linearized_ancestors_of("Bar")) end @@ -852,8 +852,8 @@ class Bar < Foo end RUBY - uri = ResourceUri.new(path: File.join(dir, "foo.rb")) - @index.index_single(uri) + indexable_path = IndexablePath.new(nil, File.join(dir, "foo.rb")) + @index.index_single(indexable_path) assert_equal(["Bar", "Foo", "Object", "Kernel", "BasicObject"], @index.linearized_ancestors_of("Bar")) @@ -866,7 +866,7 @@ class Bar end RUBY - @index.handle_change(uri) + @index.handle_change(indexable_path) assert_empty(@index.instance_variable_get(:@ancestors)) assert_equal(["Bar", "Object", "Kernel", "BasicObject"], @index.linearized_ancestors_of("Bar")) end @@ -1300,7 +1300,7 @@ class Bar < ::BasicObject; end end def test_resolving_method_inside_singleton_context - @index.index_single(ResourceUri.new(path: "/fake/path/foo.rb"), <<~RUBY) + @index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY) module Foo class Bar class << self @@ -1321,7 +1321,7 @@ def found_me!; end end def test_resolving_constants_in_singleton_contexts - @index.index_single(ResourceUri.new(path: "/fake/path/foo.rb"), <<~RUBY) + @index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY) module Foo class Bar CONST = 3 @@ -1346,7 +1346,7 @@ class << self end def test_resolving_instance_variables_in_singleton_contexts - @index.index_single(ResourceUri.new(path: "/fake/path/foo.rb"), <<~RUBY) + @index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY) module Foo class Bar @a = 123 @@ -1376,7 +1376,7 @@ def hello end def test_instance_variable_completion_in_singleton_contexts - @index.index_single(ResourceUri.new(path: "/fake/path/foo.rb"), <<~RUBY) + @index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY) module Foo class Bar @a = 123 @@ -1622,7 +1622,7 @@ def bar end def test_linearizing_singleton_ancestors_of_singleton_when_class_has_parent - @index.index_single(ResourceUri.new(path: "/fake/path/foo.rb"), <<~RUBY) + @index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY) class Foo; end class Bar < Foo @@ -1673,7 +1673,7 @@ def test_linearizing_singleton_object end def test_linearizing_singleton_ancestors - @index.index_single(ResourceUri.new(path: "/fake/path/foo.rb"), <<~RUBY) + @index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY) module First end @@ -1714,7 +1714,7 @@ class << self end def test_linearizing_singleton_ancestors_when_class_has_parent - @index.index_single(ResourceUri.new(path: "/fake/path/foo.rb"), <<~RUBY) + @index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY) class Foo; end class Bar < Foo @@ -1744,7 +1744,7 @@ class << self end def test_linearizing_a_module_singleton_class - @index.index_single(ResourceUri.new(path: "/fake/path/foo.rb"), <<~RUBY) + @index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY) module A; end RUBY diff --git a/lib/ruby_indexer/test/test_case.rb b/lib/ruby_indexer/test/test_case.rb index f79906979..88e284300 100644 --- a/lib/ruby_indexer/test/test_case.rb +++ b/lib/ruby_indexer/test/test_case.rb @@ -14,7 +14,7 @@ def setup private def index(source) - @index.index_single(ResourceUri.new(path: "/fake/path/foo.rb"), source) + @index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb"), source) end def assert_entry(expected_name, type, expected_location, visibility: nil) diff --git a/lib/ruby_lsp/listeners/completion.rb b/lib/ruby_lsp/listeners/completion.rb index 4e837cf61..20c58f408 100644 --- a/lib/ruby_lsp/listeners/completion.rb +++ b/lib/ruby_lsp/listeners/completion.rb @@ -295,9 +295,9 @@ def complete_require(node) return unless path_node_to_complete.is_a?(Prism::StringNode) - uris = @index.search_require_paths(path_node_to_complete.content) + matched_indexable_paths = @index.search_require_paths(path_node_to_complete.content) - uris.map!(&:require_path).sort!.each do |path| + matched_indexable_paths.map!(&:require_path).sort!.each do |path| @response_builder << build_completion(T.must(path), path_node_to_complete) end end diff --git a/lib/ruby_lsp/listeners/definition.rb b/lib/ruby_lsp/listeners/definition.rb index 54b59f4a8..fce061e38 100644 --- a/lib/ruby_lsp/listeners/definition.rb +++ b/lib/ruby_lsp/listeners/definition.rb @@ -235,11 +235,15 @@ def handle_method_definition(message, receiver_type, inherited_only: false) def handle_require_definition(node, message) case message when :require - uri = @index.search_require_paths(node.content).find { |uri| uri.require_path == node.content } + entry = @index.search_require_paths(node.content).find do |indexable_path| + indexable_path.require_path == node.content + end + + if entry + candidate = entry.full_path - if uri @response_builder << Interface::Location.new( - uri: uri.to_s, + uri: URI::Generic.from_path(path: candidate).to_s, range: Interface::Range.new( start: Interface::Position.new(line: 0, character: 0), end: Interface::Position.new(line: 0, character: 0), diff --git a/lib/ruby_lsp/server.rb b/lib/ruby_lsp/server.rb index eb98797b5..15b8d65d1 100644 --- a/lib/ruby_lsp/server.rb +++ b/lib/ruby_lsp/server.rb @@ -809,7 +809,7 @@ def workspace_did_change_watched_files(message) next unless file_path.end_with?(".rb") load_path_entry = $LOAD_PATH.find { |load_path| file_path.start_with?(load_path) } - indexable = RubyIndexer::ResourceUri.file(file_path, load_path_entry) + indexable = RubyIndexer::IndexablePath.new(load_path_entry, file_path) case change[:type] when Constant::FileChangeType::CREATED diff --git a/lib/ruby_lsp/test_helper.rb b/lib/ruby_lsp/test_helper.rb index 42cedf0b5..d01d2dc3b 100644 --- a/lib/ruby_lsp/test_helper.rb +++ b/lib/ruby_lsp/test_helper.rb @@ -38,7 +38,10 @@ def with_server(source = nil, uri = Kernel.URI("file:///fake.rb"), stub_no_typec }) end - server.global_state.index.index_single(RubyIndexer::ResourceUri.new(path: uri.to_standardized_path), source) + server.global_state.index.index_single( + RubyIndexer::IndexablePath.new(nil, T.must(uri.to_standardized_path)), + source, + ) server.load_addons if load_addons block.call(server, uri) ensure diff --git a/rakelib/index.rake b/rakelib/index.rake index 0c10f077a..726ae6e85 100644 --- a/rakelib/index.rake +++ b/rakelib/index.rake @@ -82,7 +82,7 @@ task "index:topgems": ["download:topgems"] do errors = Dir[File.join(directory, "**", "*.rb")].filter_map do |filepath| print(".") code = File.read(filepath) - index.index_single(RubyIndexer::ResourceUri.file(filepath), code) + index.index_single(RubyIndexer::IndexablePath.new(nil, filepath), code) nil rescue => e errors << { message: e.message, file: filepath } diff --git a/sorbet/rbi/shims/uri.rbi b/sorbet/rbi/shims/uri.rbi index 20d6abb11..e31e3ec12 100644 --- a/sorbet/rbi/shims/uri.rbi +++ b/sorbet/rbi/shims/uri.rbi @@ -3,22 +3,6 @@ module URI def self.register_scheme(scheme, klass); end - class Generic - def initialize( - scheme, - userinfo, host, port, registry, - path, opaque, - query, - fragment, - parser = DEFAULT_PARSER, - arg_check = false) - @scheme = T.let(T.unsafe(nil), String) - @path = T.let(T.unsafe(nil), T.nilable(String)) - @opaque = T.let(T.unsafe(nil), T.nilable(String)) - @fragment = T.let(T.unsafe(nil), T.nilable(String)) - end - end - class File attr_reader :path end diff --git a/test/requests/completion_test.rb b/test/requests/completion_test.rb index 3fec4bdb6..51a153acb 100644 --- a/test/requests/completion_test.rb +++ b/test/requests/completion_test.rb @@ -1339,7 +1339,7 @@ def with_file_structure(server, &block) index = server.global_state.index indexables = Dir.glob(File.join(tmpdir, "**", "*.rb")).map! do |path| - RubyIndexer::ResourceUri.file(path, tmpdir) + RubyIndexer::IndexablePath.new(tmpdir, path) end indexables.each do |indexable| diff --git a/test/requests/definition_expectations_test.rb b/test/requests/definition_expectations_test.rb index fc8fc53da..4ceea2622 100644 --- a/test/requests/definition_expectations_test.rb +++ b/test/requests/definition_expectations_test.rb @@ -15,16 +15,17 @@ def run_expectations(source) index = server.global_state.index index.index_single( - RubyIndexer::ResourceUri.file( + RubyIndexer::IndexablePath.new( + "#{Dir.pwd}/lib", File.expand_path( "../../test/fixtures/class_reference_target.rb", __dir__, ), - "#{Dir.pwd}/lib", ), ) index.index_single( - RubyIndexer::ResourceUri.file( + RubyIndexer::IndexablePath.new( + nil, File.expand_path( "../../test/fixtures/constant_reference_target.rb", __dir__, @@ -32,12 +33,12 @@ def run_expectations(source) ), ) index.index_single( - RubyIndexer::ResourceUri.file( + RubyIndexer::IndexablePath.new( + "#{Dir.pwd}/lib", File.expand_path( "../../lib/ruby_lsp/server.rb", __dir__, ), - "#{Dir.pwd}/lib", ), ) @@ -76,7 +77,10 @@ def test_jumping_to_default_gems with_server("Pathname") do |server, uri| index = server.global_state.index index.index_single( - RubyIndexer::ResourceUri.new(path: "#{RbConfig::CONFIG["rubylibdir"]}/pathname.rb"), + RubyIndexer::IndexablePath.new( + nil, + "#{RbConfig::CONFIG["rubylibdir"]}/pathname.rb", + ), ) server.process_message( id: 1, @@ -138,12 +142,12 @@ def test_jumping_to_default_require_of_a_gem bundler_uri = URI::Generic.from_path(path: "#{RbConfig::CONFIG["rubylibdir"]}/bundler.rb") index.index_single( - RubyIndexer::ResourceUri.file(T.must(bundler_uri.to_standardized_path), RbConfig::CONFIG["rubylibdir"]), + RubyIndexer::IndexablePath.new(RbConfig::CONFIG["rubylibdir"], T.must(bundler_uri.to_standardized_path)), ) Dir.glob("#{RbConfig::CONFIG["rubylibdir"]}/bundler/*.rb").each do |path| index.index_single( - RubyIndexer::ResourceUri.file(path, RbConfig::CONFIG["rubylibdir"]), + RubyIndexer::IndexablePath.new(RbConfig::CONFIG["rubylibdir"], path), ) end @@ -209,12 +213,12 @@ def test_definition_addons with_server(source, stub_no_typechecker: true) do |server, uri| server.global_state.index.index_single( - RubyIndexer::ResourceUri.new( - path: File.expand_path( + RubyIndexer::IndexablePath.new( + "#{Dir.pwd}/lib", + File.expand_path( "../../test/fixtures/class_reference_target.rb", __dir__, ), - require_path: "#{Dir.pwd}/lib", ), ) server.process_message( @@ -290,7 +294,7 @@ def foo; end }, }) index = server.global_state.index - index.index_single(RubyIndexer::ResourceUri.new(path: T.must(second_uri.to_standardized_path)), second_source) + index.index_single(RubyIndexer::IndexablePath.new(nil, T.must(second_uri.to_standardized_path)), second_source) server.process_message( id: 1, @@ -359,12 +363,12 @@ class Foo with_server(source) do |server, uri| server.global_state.index.index_single( - RubyIndexer::ResourceUri.new(path: "/fake/path/bar.rb"), <<~RUBY + RubyIndexer::IndexablePath.new(nil, "/fake/path/bar.rb"), <<~RUBY class Foo::Bar; end RUBY ) server.global_state.index.index_single( - RubyIndexer::ResourceUri.new(path: "/fake/path/baz.rb"), <<~RUBY + RubyIndexer::IndexablePath.new(nil, "/fake/path/baz.rb"), <<~RUBY class Foo::Bar; end RUBY ) @@ -564,7 +568,7 @@ def test_definitions_are_listed_in_erb_files_as_unknown_receiver with_server(source, URI("/fake.erb")) do |server, uri| server.global_state.index.index_single( - RubyIndexer::ResourceUri.new(path: "/fake/path/foo.rb"), <<~RUBY + RubyIndexer::IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY class Bar def foo; end diff --git a/lib/ruby_indexer/test/uri_test.rb b/test/requests/support/uri_test.rb similarity index 65% rename from lib/ruby_indexer/test/uri_test.rb rename to test/requests/support/uri_test.rb index 519b10016..8539eafc1 100644 --- a/lib/ruby_indexer/test/uri_test.rb +++ b/test/requests/support/uri_test.rb @@ -3,35 +3,35 @@ require "test_helper" -module RubyIndexer - class UriTest < Minitest::Test +module RubyLsp + class URITest < Minitest::Test def test_from_path_on_unix - uri = ResourceUri.new(path: "/some/unix/path/to/file.rb") + uri = URI::Generic.from_path(path: "/some/unix/path/to/file.rb") assert_equal("/some/unix/path/to/file.rb", uri.path) end def test_from_path_on_windows - uri = ResourceUri.new(path: "C:/some/windows/path/to/file.rb") + uri = URI::Generic.from_path(path: "C:/some/windows/path/to/file.rb") assert_equal("/C:/some/windows/path/to/file.rb", uri.path) end def test_from_path_on_windows_with_lowercase_drive - uri = ResourceUri.new(path: "c:/some/windows/path/to/file.rb") + uri = URI::Generic.from_path(path: "c:/some/windows/path/to/file.rb") assert_equal("/c:/some/windows/path/to/file.rb", uri.path) end def test_to_standardized_path_on_unix - uri = ResourceUri.new(path: "/some/unix/path/to/file.rb") + uri = URI::Generic.from_path(path: "/some/unix/path/to/file.rb") assert_equal(uri.path, uri.to_standardized_path) end def test_to_standardized_path_on_windows - uri = ResourceUri.new(path: "C:/some/windows/path/to/file.rb") + uri = URI::Generic.from_path(path: "C:/some/windows/path/to/file.rb") assert_equal("C:/some/windows/path/to/file.rb", uri.to_standardized_path) end def test_to_standardized_path_on_windows_with_lowercase_drive - uri = ResourceUri.new(path: "c:/some/windows/path/to/file.rb") + uri = URI::Generic.from_path(path: "c:/some/windows/path/to/file.rb") assert_equal("c:/some/windows/path/to/file.rb", uri.to_standardized_path) end @@ -42,18 +42,17 @@ def test_to_standardized_path_on_windows_with_received_uri def test_plus_signs_are_properly_unescaped path = "/opt/rubies/3.3.0/lib/ruby/3.3.0+0/pathname.rb" - uri = ResourceUri.new(path: path) + uri = URI::Generic.from_path(path: path) assert_equal(path, uri.to_standardized_path) end def test_from_path_with_fragment - uri = ResourceUri.new(path: "/some/unix/path/to/file.rb", fragment: "L1,3-2,9") - assert_equal("file:///some/unix/path/to/file.rb", uri.to_s) - assert_equal("file:///some/unix/path/to/file.rb#L1,3-2,9", uri.to_s_with_fragment) + uri = URI::Generic.from_path(path: "/some/unix/path/to/file.rb", fragment: "L1,3-2,9") + assert_equal("file:///some/unix/path/to/file.rb#L1,3-2,9", uri.to_s) end def test_from_path_windows_long_file_paths - uri = ResourceUri.new(path: "//?/C:/hostedtoolcache/windows/Ruby/3.3.1/x64/lib/ruby/3.3.0/open-uri.rb") + uri = URI::Generic.from_path(path: "//?/C:/hostedtoolcache/windows/Ruby/3.3.1/x64/lib/ruby/3.3.0/open-uri.rb") assert_equal("C:/hostedtoolcache/windows/Ruby/3.3.1/x64/lib/ruby/3.3.0/open-uri.rb", uri.to_standardized_path) end end diff --git a/test/requests/workspace_symbol_test.rb b/test/requests/workspace_symbol_test.rb index 400169c5b..76bf165a2 100644 --- a/test/requests/workspace_symbol_test.rb +++ b/test/requests/workspace_symbol_test.rb @@ -11,7 +11,7 @@ def setup end def test_returns_index_entries_based_on_query - @index.index_single(RubyIndexer::ResourceUri.new(path: "/fake.rb"), <<~RUBY) + @index.index_single(RubyIndexer::IndexablePath.new(nil, "/fake.rb"), <<~RUBY) class Foo; end module Bar; end @@ -32,7 +32,7 @@ module Bar; end end def test_fuzzy_matches_symbols - @index.index_single(RubyIndexer::ResourceUri.new(path: "/fake.rb"), <<~RUBY) + @index.index_single(RubyIndexer::IndexablePath.new(nil, "/fake.rb"), <<~RUBY) class Foo; end module Bar; end @@ -53,7 +53,7 @@ module Bar; end end def test_symbols_include_container_name - @index.index_single(RubyIndexer::ResourceUri.new(path: "/fake.rb"), <<~RUBY) + @index.index_single(RubyIndexer::IndexablePath.new(nil, "/fake.rb"), <<~RUBY) module Foo class Bar; end end @@ -66,14 +66,14 @@ class Bar; end end def test_does_not_include_symbols_from_dependencies - @index.index_single(RubyIndexer::ResourceUri.new(path: "#{RbConfig::CONFIG["rubylibdir"]}/pathname.rb")) + @index.index_single(RubyIndexer::IndexablePath.new(nil, "#{RbConfig::CONFIG["rubylibdir"]}/pathname.rb")) result = RubyLsp::Requests::WorkspaceSymbol.new(@global_state, "Pathname").perform assert_empty(result) end def test_does_not_include_private_constants - @index.index_single(RubyIndexer::ResourceUri.new(path: "/fake.rb"), <<~RUBY) + @index.index_single(RubyIndexer::IndexablePath.new(nil, "/fake.rb"), <<~RUBY) class Foo CONSTANT = 1 private_constant(:CONSTANT) @@ -86,7 +86,7 @@ class Foo end def test_returns_method_symbols - @index.index_single(RubyIndexer::ResourceUri.new(path: "/fake.rb"), <<~RUBY) + @index.index_single(RubyIndexer::IndexablePath.new(nil, "/fake.rb"), <<~RUBY) class Foo attr_reader :baz diff --git a/test/server_test.rb b/test/server_test.rb index 232a22e0f..d18345176 100644 --- a/test/server_test.rb +++ b/test/server_test.rb @@ -408,8 +408,8 @@ def test_backtrace_is_printed_to_stderr_on_exceptions end def test_changed_file_only_indexes_ruby - @server.global_state.index.expects(:index_single).once.with do |uri| - uri.to_standardized_path == "/foo.rb" + @server.global_state.index.expects(:index_single).once.with do |indexable| + indexable.full_path == "/foo.rb" end @server.process_message({ method: "workspace/didChangeWatchedFiles", diff --git a/test/type_inferrer_test.rb b/test/type_inferrer_test.rb index c3482451a..16b3d0cde 100644 --- a/test/type_inferrer_test.rb +++ b/test/type_inferrer_test.rb @@ -361,7 +361,7 @@ def test_infer_lambda_literal private def index_and_locate(source, position) - @index.index_single(RubyIndexer::ResourceUri.new(path: "/fake/path/foo.rb"), source) + @index.index_single(RubyIndexer::IndexablePath.new(nil, "/fake/path/foo.rb"), source) document = RubyLsp::RubyDocument.new( source: source, version: 1,