diff --git a/exe/ruby-lsp b/exe/ruby-lsp index 9e551f471..dd318326e 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 |indexable| - puts "indexing: #{indexable.full_path}" - index.index_single(indexable) + index.configuration.indexables.each do |uri| + puts "indexing: #{uri}" + index.index_single(uri) end return end diff --git a/exe/ruby-lsp-check b/exe/ruby-lsp-check index ad2d3f6b3..0b3f71f5e 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 |indexable, i| - index.index_single(indexable) +indexables.each_with_index do |uri, i| + index.index_single(uri) rescue => e - errors[indexable.full_path] = e + errors[uri.to_standarized_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 330c64b70..ff0cb500c 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[IndexablePath]) } + sig { returns(T::Array[FileUri]) } 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 - IndexablePath.new(load_path_entry, path) + ResourceUri.file(path, load_path_entry) 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.full_path, File::FNM_PATHNAME | File::FNM_EXTGLOB) + File.fnmatch?(pattern, indexable.to_standardized_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| - IndexablePath.new(RbConfig::CONFIG["rubylibdir"], path) + ResourceUri.file(path, RbConfig::CONFIG["rubylibdir"]) end, ) elsif pathname.extname == ".rb" # If the default_path is a Ruby file, we index it - indexables << IndexablePath.new(RbConfig::CONFIG["rubylibdir"], default_path) + indexables << ResourceUri.file(default_path, RbConfig::CONFIG["rubylibdir"]) 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| IndexablePath.new(load_path_entry, path) } + Dir.glob(File.join(load_path_entry, "**", "*.rb")).map! { |path| ResourceUri.file(path, load_path_entry) } end, ) rescue Gem::MissingSpecError @@ -155,7 +155,7 @@ def indexables # just ignore if they're missing end - indexables.uniq!(&:full_path) + indexables.uniq!(&:to_s) indexables end diff --git a/lib/ruby_indexer/lib/ruby_indexer/index.rb b/lib/ruby_indexer/lib/ruby_indexer/index.rb index dc2353408..6741c1d14 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[IndexablePath].new, PrefixTree[IndexablePath]) + @require_paths_tree = T.let(PrefixTree[ResourceUri].new, PrefixTree[ResourceUri]) # Holds the linearized ancestors list for every namespace @ancestors = T.let({}, T::Hash[String, T::Array[String]]) @@ -63,31 +63,35 @@ def register_included_hook(module_name, &hook) (@included_hooks[module_name] ||= []) << hook 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 - - # 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) + 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 - end - @files_to_entries.delete(indexable.full_path) + @files_to_entries.delete(path) + end - require_path = indexable.require_path + require_path = uri.require_path @require_paths_tree.delete(require_path) if require_path end @@ -105,7 +109,7 @@ def [](fully_qualified_name) @entries[fully_qualified_name.delete_prefix("::")] end - sig { params(query: String).returns(T::Array[IndexablePath]) } + sig { params(query: String).returns(T::Array[ResourceUri]) } def search_require_paths(query) @require_paths_tree.search(query) end @@ -292,33 +296,37 @@ def resolve(name, nesting, seen_names = []) nil end - # 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. + # 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. sig do params( - indexable_paths: T::Array[IndexablePath], + uris: T::Array[ResourceUri], block: T.nilable(T.proc.params(progress: Integer).returns(T::Boolean)), ).void end - def index_all(indexable_paths: @configuration.indexables, &block) + def index_all(uris: @configuration.indexables, &block) RBSIndexer.new(self).index_ruby_core # Calculate how many paths are worth 1% of progress - progress_step = (indexable_paths.length / 100.0).ceil + progress_step = (uris.length / 100.0).ceil - indexable_paths.each_with_index do |path, index| + uris.each_with_index do |uri, index| if block && index % progress_step == 0 progress = (index / progress_step) + 1 break unless block.call(progress) end - index_single(path) + index_single(uri) end end - 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) + 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) dispatcher = Prism::Dispatcher.new result = Prism.parse(content) @@ -326,15 +334,15 @@ def index_single(indexable_path, source = nil) self, dispatcher, result, - indexable_path.full_path, + path, enhancements: @enhancements, ) dispatcher.dispatch(result.value) indexing_errors = listener.indexing_errors.uniq - require_path = indexable_path.require_path - @require_paths_tree.insert(require_path, indexable_path) if require_path + require_path = uri.require_path + @require_paths_tree.insert(require_path, uri) if require_path if indexing_errors.any? indexing_errors.each do |error| @@ -346,7 +354,7 @@ def index_single(indexable_path, source = nil) # it rescue SystemStackError => e if e.backtrace&.first&.include?("prism") - $stderr.puts "Prism error indexing #{indexable_path.full_path}: #{e.message}" + $stderr.puts "Prism error indexing #{uri}: #{e.message}" else raise end @@ -543,16 +551,17 @@ def instance_variable_completion_candidates(name, owner_name) variables end - # 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] + # 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] - delete(indexable) - index_single(indexable) + delete(uri) + index_single(uri) - updated_entries = @files_to_entries[indexable.full_path] + updated_entries = @files_to_entries[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 deleted file mode 100644 index 2d2b1d585..000000000 --- a/lib/ruby_indexer/lib/ruby_indexer/indexable_path.rb +++ /dev/null @@ -1,29 +0,0 @@ -# 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 new file mode 100644 index 000000000..7f6813e8d --- /dev/null +++ b/lib/ruby_indexer/lib/ruby_indexer/resource_uri.rb @@ -0,0 +1,105 @@ +# 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 996af2f8c..8b0994e9d 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/indexable_path" +require "ruby_indexer/lib/ruby_indexer/resource_uri" 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 eae6d6a45..a9e176e95 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(IndexablePath.new(nil, "/fake/path/foo.rb")) + @index.delete(ResourceUri.new(path: "/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 2078571c0..1c76d68e8 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.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__ }) + 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__ }) 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?(indexable.full_path) }) + assert(indexables.all? { |indexable| File.absolute_path?(T.must(indexable.to_standardized_path)) }) end def test_indexables_only_includes_gem_require_paths @@ -37,7 +37,9 @@ 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? { |indexable| indexable.full_path.start_with?("#{spec.full_gem_path}/test/") }) + assert(indexables.none? do |indexable| + indexable.to_standardized_path.start_with?("#{spec.full_gem_path}/test/") + end) rescue Gem::MissingSpecError # Transitive dependencies might be missing when running tests on Windows end @@ -47,12 +49,14 @@ def test_indexables_does_not_include_default_gem_path_when_in_bundle indexables = @config.indexables assert( - indexables.none? { |indexable| indexable.full_path.start_with?("#{RbConfig::CONFIG["rubylibdir"]}/psych") }, + indexables.none? do |indexable| + indexable.to_standardized_path.start_with?("#{RbConfig::CONFIG["rubylibdir"]}/psych") + end, ) end def test_indexables_includes_default_gems - indexables = @config.indexables.map(&:full_path) + indexables = @config.indexables.map(&:to_standardized_path) assert_includes(indexables, "#{RbConfig::CONFIG["rubylibdir"]}/pathname.rb") assert_includes(indexables, "#{RbConfig::CONFIG["rubylibdir"]}/ipaddr.rb") @@ -60,7 +64,7 @@ def test_indexables_includes_default_gems end def test_indexables_includes_project_files - indexables = @config.indexables.map(&:full_path) + indexables = @config.indexables.map(&:to_standardized_path) Dir.glob("#{Dir.pwd}/lib/**/*.rb").each do |path| next if path.end_with?("_test.rb") @@ -80,13 +84,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.full_path.start_with?(Bundler.bundle_path.join("gems", "ruby-lsp").to_s) + indexable.to_standardized_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(&:full_path)}", + "Included: #{indexables_inside_bundled_lsp.map(&:to_standardized_path)}", ) end @@ -95,7 +99,7 @@ def test_indexables_does_not_include_non_ruby_files_inside_rubylibdir FileUtils.touch(path) indexables = @config.indexables - assert(indexables.none? { |indexable| indexable.full_path == path }) + assert(indexables.none? { |indexable| indexable.to_standardized_path == path }) ensure FileUtils.rm(T.must(path)) end @@ -143,7 +147,7 @@ def test_indexables_respect_given_workspace_path @config.workspace_path = dir indexables = @config.indexables - assert(indexables.none? { |indexable| indexable.full_path.start_with?(File.join(dir, "ignore")) }) + assert(indexables.none? { |indexable| indexable.to_standardized_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 @@ -152,10 +156,10 @@ def test_indexables_respect_given_workspace_path # - Default gems assert( indexables.all? do |i| - 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"]) + 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"]) end, ) end diff --git a/lib/ruby_indexer/test/index_test.rb b/lib/ruby_indexer/test/index_test.rb index 7e1f5a111..cb158848e 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(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY) + @index.index_single(ResourceUri.new(path: "/fake/path/foo.rb"), <<~RUBY) class Foo end RUBY - @index.index_single(IndexablePath.new(nil, "/fake/path/other_foo.rb"), <<~RUBY) + @index.index_single(ResourceUri.new(path: "/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(IndexablePath.new(nil, "/fake/path/other_foo.rb")) + @index.delete(ResourceUri.new(path: "/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(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY) + @index.index_single(ResourceUri.new(path: "/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(IndexablePath.new(nil, "/fake/path/foo.rb")) + @index.delete(ResourceUri.new(path: "/fake/path/foo.rb")) entries = @index["Foo"] assert_nil(entries) end def test_index_resolve - @index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY) + @index.index_single(ResourceUri.new(path: "/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(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY) + @index.index_single(ResourceUri.new(path: "/fake/path/foo.rb"), <<~RUBY) class Bar; end module Foo @@ -92,7 +92,7 @@ class Something end def test_fuzzy_search - @index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY) + @index.index_single(ResourceUri.new(path: "/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(IndexablePath.new(nil, "lib/this_is_a_dir.rb")) + @index.index_single(ResourceUri.new(path: File.expand_path("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(IndexablePath.new("/fake", "/fake/path/foo.rb"), <<~RUBY) + @index.index_single(ResourceUri.new(path: "/fake/path/foo.rb", require_path: "path/foo"), <<~RUBY) class Foo end RUBY - @index.index_single(IndexablePath.new("/fake", "/fake/path/other_foo.rb"), <<~RUBY) + @index.index_single(ResourceUri.new(path: "/fake/path/other_foo.rb", require_path: "path/other_foo"), <<~RUBY) class Foo end RUBY @@ -140,11 +140,11 @@ class Foo end def test_searching_for_entries_based_on_prefix - @index.index_single(IndexablePath.new("/fake", "/fake/path/foo.rb"), <<~RUBY) + @index.index_single(ResourceUri.new(path: "/fake/path/foo.rb", require_path: "path/foo"), <<~RUBY) class Foo::Bizw end RUBY - @index.index_single(IndexablePath.new("/fake", "/fake/path/other_foo.rb"), <<~RUBY) + @index.index_single(ResourceUri.new(path: "/fake/path/other_foo.rb", require_path: "path/other_foo"), <<~RUBY) class Foo::Bizw end @@ -160,7 +160,7 @@ class Foo::Bizt end def test_resolve_normalizes_top_level_names - @index.index_single(IndexablePath.new("/fake", "/fake/path/foo.rb"), <<~RUBY) + @index.index_single(ResourceUri.new(path: "/fake/path/foo.rb", require_path: "path/foo"), <<~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(IndexablePath.new("/fake", "/fake/path/foo.rb"), <<~RUBY) + @index.index_single(ResourceUri.new(require_path: "path/foo", path: "/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| - indexable_path = IndexablePath.new("", fixture) - @index.index_single(indexable_path) + uri = ResourceUri.new(path: File.expand_path(fixture)) + @index.index_single(uri) end refute_empty(@index) end def test_index_single_does_not_fail_for_non_existing_file - @index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb")) + @index.index_single(ResourceUri.new(path: "/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 - indexable_path = IndexablePath.new(nil, File.join(dir, "foo.rb")) - @index.index_single(indexable_path) + uri = ResourceUri.new(path: File.join(dir, "foo.rb")) + @index.index_single(uri) assert_equal(["Bar", "Foo", "Object", "Kernel", "BasicObject"], @index.linearized_ancestors_of("Bar")) @@ -796,7 +796,7 @@ class Bar end RUBY - @index.handle_change(indexable_path) + @index.handle_change(uri) 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 - indexable_path = IndexablePath.new(nil, File.join(dir, "foo.rb")) - @index.index_single(indexable_path) + uri = ResourceUri.new(path: File.join(dir, "foo.rb")) + @index.index_single(uri) assert_equal(["Bar", "Foo", "Object", "Kernel", "BasicObject"], @index.linearized_ancestors_of("Bar")) @@ -833,7 +833,7 @@ def baz; end end RUBY - @index.handle_change(indexable_path) + @index.handle_change(uri) 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 - indexable_path = IndexablePath.new(nil, File.join(dir, "foo.rb")) - @index.index_single(indexable_path) + uri = ResourceUri.new(path: File.join(dir, "foo.rb")) + @index.index_single(uri) assert_equal(["Bar", "Foo", "Object", "Kernel", "BasicObject"], @index.linearized_ancestors_of("Bar")) @@ -866,7 +866,7 @@ class Bar end RUBY - @index.handle_change(indexable_path) + @index.handle_change(uri) 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(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY) + @index.index_single(ResourceUri.new(path: "/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(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY) + @index.index_single(ResourceUri.new(path: "/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(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY) + @index.index_single(ResourceUri.new(path: "/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(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY) + @index.index_single(ResourceUri.new(path: "/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(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY) + @index.index_single(ResourceUri.new(path: "/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(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY) + @index.index_single(ResourceUri.new(path: "/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(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY) + @index.index_single(ResourceUri.new(path: "/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(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY) + @index.index_single(ResourceUri.new(path: "/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 88e284300..f79906979 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(IndexablePath.new(nil, "/fake/path/foo.rb"), source) + @index.index_single(ResourceUri.new(path: "/fake/path/foo.rb"), source) end def assert_entry(expected_name, type, expected_location, visibility: nil) diff --git a/test/requests/support/uri_test.rb b/lib/ruby_indexer/test/uri_test.rb similarity index 65% rename from test/requests/support/uri_test.rb rename to lib/ruby_indexer/test/uri_test.rb index 8539eafc1..519b10016 100644 --- a/test/requests/support/uri_test.rb +++ b/lib/ruby_indexer/test/uri_test.rb @@ -3,35 +3,35 @@ require "test_helper" -module RubyLsp - class URITest < Minitest::Test +module RubyIndexer + class UriTest < Minitest::Test def test_from_path_on_unix - uri = URI::Generic.from_path(path: "/some/unix/path/to/file.rb") + uri = ResourceUri.new(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 = URI::Generic.from_path(path: "C:/some/windows/path/to/file.rb") + uri = ResourceUri.new(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 = URI::Generic.from_path(path: "c:/some/windows/path/to/file.rb") + uri = ResourceUri.new(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 = URI::Generic.from_path(path: "/some/unix/path/to/file.rb") + uri = ResourceUri.new(path: "/some/unix/path/to/file.rb") assert_equal(uri.path, uri.to_standardized_path) end def test_to_standardized_path_on_windows - uri = URI::Generic.from_path(path: "C:/some/windows/path/to/file.rb") + uri = ResourceUri.new(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 = URI::Generic.from_path(path: "c:/some/windows/path/to/file.rb") + uri = ResourceUri.new(path: "c:/some/windows/path/to/file.rb") assert_equal("c:/some/windows/path/to/file.rb", uri.to_standardized_path) end @@ -42,17 +42,18 @@ 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 = URI::Generic.from_path(path: path) + uri = ResourceUri.new(path: path) assert_equal(path, uri.to_standardized_path) end def test_from_path_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) + 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) end def test_from_path_windows_long_file_paths - uri = URI::Generic.from_path(path: "//?/C:/hostedtoolcache/windows/Ruby/3.3.1/x64/lib/ruby/3.3.0/open-uri.rb") + uri = ResourceUri.new(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/lib/ruby_lsp/listeners/completion.rb b/lib/ruby_lsp/listeners/completion.rb index 20c58f408..4e837cf61 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) - matched_indexable_paths = @index.search_require_paths(path_node_to_complete.content) + uris = @index.search_require_paths(path_node_to_complete.content) - matched_indexable_paths.map!(&:require_path).sort!.each do |path| + uris.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 fce061e38..54b59f4a8 100644 --- a/lib/ruby_lsp/listeners/definition.rb +++ b/lib/ruby_lsp/listeners/definition.rb @@ -235,15 +235,11 @@ def handle_method_definition(message, receiver_type, inherited_only: false) def handle_require_definition(node, message) case message when :require - entry = @index.search_require_paths(node.content).find do |indexable_path| - indexable_path.require_path == node.content - end - - if entry - candidate = entry.full_path + uri = @index.search_require_paths(node.content).find { |uri| uri.require_path == node.content } + if uri @response_builder << Interface::Location.new( - uri: URI::Generic.from_path(path: candidate).to_s, + uri: uri.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 15b8d65d1..eb98797b5 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::IndexablePath.new(load_path_entry, file_path) + indexable = RubyIndexer::ResourceUri.file(file_path, load_path_entry) case change[:type] when Constant::FileChangeType::CREATED diff --git a/lib/ruby_lsp/test_helper.rb b/lib/ruby_lsp/test_helper.rb index d01d2dc3b..42cedf0b5 100644 --- a/lib/ruby_lsp/test_helper.rb +++ b/lib/ruby_lsp/test_helper.rb @@ -38,10 +38,7 @@ def with_server(source = nil, uri = Kernel.URI("file:///fake.rb"), stub_no_typec }) end - server.global_state.index.index_single( - RubyIndexer::IndexablePath.new(nil, T.must(uri.to_standardized_path)), - source, - ) + server.global_state.index.index_single(RubyIndexer::ResourceUri.new(path: 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 726ae6e85..0c10f077a 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::IndexablePath.new(nil, filepath), code) + index.index_single(RubyIndexer::ResourceUri.file(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 e31e3ec12..20d6abb11 100644 --- a/sorbet/rbi/shims/uri.rbi +++ b/sorbet/rbi/shims/uri.rbi @@ -3,6 +3,22 @@ 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 51a153acb..3fec4bdb6 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::IndexablePath.new(tmpdir, path) + RubyIndexer::ResourceUri.file(path, tmpdir) end indexables.each do |indexable| diff --git a/test/requests/definition_expectations_test.rb b/test/requests/definition_expectations_test.rb index 4ceea2622..fc8fc53da 100644 --- a/test/requests/definition_expectations_test.rb +++ b/test/requests/definition_expectations_test.rb @@ -15,17 +15,16 @@ def run_expectations(source) index = server.global_state.index index.index_single( - RubyIndexer::IndexablePath.new( - "#{Dir.pwd}/lib", + RubyIndexer::ResourceUri.file( File.expand_path( "../../test/fixtures/class_reference_target.rb", __dir__, ), + "#{Dir.pwd}/lib", ), ) index.index_single( - RubyIndexer::IndexablePath.new( - nil, + RubyIndexer::ResourceUri.file( File.expand_path( "../../test/fixtures/constant_reference_target.rb", __dir__, @@ -33,12 +32,12 @@ def run_expectations(source) ), ) index.index_single( - RubyIndexer::IndexablePath.new( - "#{Dir.pwd}/lib", + RubyIndexer::ResourceUri.file( File.expand_path( "../../lib/ruby_lsp/server.rb", __dir__, ), + "#{Dir.pwd}/lib", ), ) @@ -77,10 +76,7 @@ def test_jumping_to_default_gems with_server("Pathname") do |server, uri| index = server.global_state.index index.index_single( - RubyIndexer::IndexablePath.new( - nil, - "#{RbConfig::CONFIG["rubylibdir"]}/pathname.rb", - ), + RubyIndexer::ResourceUri.new(path: "#{RbConfig::CONFIG["rubylibdir"]}/pathname.rb"), ) server.process_message( id: 1, @@ -142,12 +138,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::IndexablePath.new(RbConfig::CONFIG["rubylibdir"], T.must(bundler_uri.to_standardized_path)), + RubyIndexer::ResourceUri.file(T.must(bundler_uri.to_standardized_path), RbConfig::CONFIG["rubylibdir"]), ) Dir.glob("#{RbConfig::CONFIG["rubylibdir"]}/bundler/*.rb").each do |path| index.index_single( - RubyIndexer::IndexablePath.new(RbConfig::CONFIG["rubylibdir"], path), + RubyIndexer::ResourceUri.file(path, RbConfig::CONFIG["rubylibdir"]), ) end @@ -213,12 +209,12 @@ def test_definition_addons with_server(source, stub_no_typechecker: true) do |server, uri| server.global_state.index.index_single( - RubyIndexer::IndexablePath.new( - "#{Dir.pwd}/lib", - File.expand_path( + RubyIndexer::ResourceUri.new( + path: File.expand_path( "../../test/fixtures/class_reference_target.rb", __dir__, ), + require_path: "#{Dir.pwd}/lib", ), ) server.process_message( @@ -294,7 +290,7 @@ def foo; end }, }) index = server.global_state.index - index.index_single(RubyIndexer::IndexablePath.new(nil, T.must(second_uri.to_standardized_path)), second_source) + index.index_single(RubyIndexer::ResourceUri.new(path: T.must(second_uri.to_standardized_path)), second_source) server.process_message( id: 1, @@ -363,12 +359,12 @@ class Foo with_server(source) do |server, uri| server.global_state.index.index_single( - RubyIndexer::IndexablePath.new(nil, "/fake/path/bar.rb"), <<~RUBY + RubyIndexer::ResourceUri.new(path: "/fake/path/bar.rb"), <<~RUBY class Foo::Bar; end RUBY ) server.global_state.index.index_single( - RubyIndexer::IndexablePath.new(nil, "/fake/path/baz.rb"), <<~RUBY + RubyIndexer::ResourceUri.new(path: "/fake/path/baz.rb"), <<~RUBY class Foo::Bar; end RUBY ) @@ -568,7 +564,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::IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY + RubyIndexer::ResourceUri.new(path: "/fake/path/foo.rb"), <<~RUBY class Bar def foo; end diff --git a/test/requests/workspace_symbol_test.rb b/test/requests/workspace_symbol_test.rb index 76bf165a2..400169c5b 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::IndexablePath.new(nil, "/fake.rb"), <<~RUBY) + @index.index_single(RubyIndexer::ResourceUri.new(path: "/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::IndexablePath.new(nil, "/fake.rb"), <<~RUBY) + @index.index_single(RubyIndexer::ResourceUri.new(path: "/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::IndexablePath.new(nil, "/fake.rb"), <<~RUBY) + @index.index_single(RubyIndexer::ResourceUri.new(path: "/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::IndexablePath.new(nil, "#{RbConfig::CONFIG["rubylibdir"]}/pathname.rb")) + @index.index_single(RubyIndexer::ResourceUri.new(path: "#{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::IndexablePath.new(nil, "/fake.rb"), <<~RUBY) + @index.index_single(RubyIndexer::ResourceUri.new(path: "/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::IndexablePath.new(nil, "/fake.rb"), <<~RUBY) + @index.index_single(RubyIndexer::ResourceUri.new(path: "/fake.rb"), <<~RUBY) class Foo attr_reader :baz diff --git a/test/server_test.rb b/test/server_test.rb index d18345176..232a22e0f 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 |indexable| - indexable.full_path == "/foo.rb" + @server.global_state.index.expects(:index_single).once.with do |uri| + uri.to_standardized_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 16b3d0cde..c3482451a 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::IndexablePath.new(nil, "/fake/path/foo.rb"), source) + @index.index_single(RubyIndexer::ResourceUri.new(path: "/fake/path/foo.rb"), source) document = RubyLsp::RubyDocument.new( source: source, version: 1,