ruby-lsp 0.9.3 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0c2f16b0f0e93ade0e4465fcf71148ac156f6d65b1354f07af2df4007c600c15
4
- data.tar.gz: 7c6f4668f461315039e37f04354dddadb1678dd89ff9c249a268897992dfa4c0
3
+ metadata.gz: 98731986ff16a22d1854fe971bfa188d55212665128ad635aabbf519c1097d9f
4
+ data.tar.gz: fe416e79e672448b3d2477909fec47451c6f878020ec92e2178259d933c51cb4
5
5
  SHA512:
6
- metadata.gz: 184683630988346ae617e3df3d6d04c66b822d6d9734f595a41caa3603b38046e5ee5a7a4361abede0f3b8d1584371730be9af905d006fa881d4b718f3381947
7
- data.tar.gz: 7c10ef841c059110b1d5bc38cde57de243140c37d7095ab8c7ce16d28e4c520c402f2bfcf604f7002a7e06d7db031b8ab9e52ce28bf3c50be0dc2671d5728170
6
+ metadata.gz: ad9a36886ffb6b1370b2530ffdb84a89244e22ec2329ee111d70bea8e90ad7ebe6c92b87dd437e283270cb09a7d564b3471d7ddcdc51df86df54d4bdfd09ada8
7
+ data.tar.gz: 9a89e7a763d1a74a320ca8dd544a52ead86a3bd573204ad86585dce7dc7797ac993f3716b98e9e6cdfd9d5b6b7a8f17e9743aa149866945d099e7d9210719a28
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.9.3
1
+ 0.10.0
@@ -24,11 +24,11 @@ module RubyIndexer
24
24
 
25
25
  @excluded_gems = T.let(excluded_gem_names, T::Array[String])
26
26
  @included_gems = T.let([], T::Array[String])
27
- @excluded_patterns = T.let(["**/*_test.rb"], T::Array[String])
27
+ @excluded_patterns = T.let([File.join("**", "*_test.rb")], T::Array[String])
28
28
  path = Bundler.settings["path"]
29
- @excluded_patterns << "#{File.expand_path(path, Dir.pwd)}/**/*.rb" if path
29
+ @excluded_patterns << File.join(File.expand_path(path, Dir.pwd), "**", "*.rb") if path
30
30
 
31
- @included_patterns = T.let(["#{Dir.pwd}/**/*.rb"], T::Array[String])
31
+ @included_patterns = T.let([File.join(Dir.pwd, "**", "*.rb")], T::Array[String])
32
32
  @excluded_magic_comments = T.let(
33
33
  [
34
34
  "frozen_string_literal:",
@@ -61,8 +61,8 @@ module RubyIndexer
61
61
  raise e, "Syntax error while loading .index.yml configuration: #{e.message}"
62
62
  end
63
63
 
64
- sig { returns(T::Array[String]) }
65
- def files_to_index
64
+ sig { returns(T::Array[IndexablePath]) }
65
+ def indexables
66
66
  excluded_gems = @excluded_gems - @included_gems
67
67
  locked_gems = Bundler.locked_gems&.specs
68
68
 
@@ -70,19 +70,22 @@ module RubyIndexer
70
70
  # having duplicates if BUNDLE_PATH is set to a folder inside the project structure
71
71
 
72
72
  # Add user specified patterns
73
- files_to_index = @included_patterns.flat_map do |pattern|
74
- Dir.glob(pattern, File::FNM_PATHNAME | File::FNM_EXTGLOB)
73
+ indexables = @included_patterns.flat_map do |pattern|
74
+ Dir.glob(pattern, File::FNM_PATHNAME | File::FNM_EXTGLOB).map! do |path|
75
+ load_path_entry = $LOAD_PATH.find { |load_path| path.start_with?(load_path) }
76
+ IndexablePath.new(load_path_entry, path)
77
+ end
75
78
  end
76
79
 
77
80
  # Remove user specified patterns
78
- files_to_index.reject! do |path|
81
+ indexables.reject! do |indexable|
79
82
  @excluded_patterns.any? do |pattern|
80
- File.fnmatch?(pattern, path, File::FNM_PATHNAME | File::FNM_EXTGLOB)
83
+ File.fnmatch?(pattern, indexable.full_path, File::FNM_PATHNAME | File::FNM_EXTGLOB)
81
84
  end
82
85
  end
83
86
 
84
87
  # Add default gems to the list of files to be indexed
85
- Dir.glob("#{RbConfig::CONFIG["rubylibdir"]}/*").each do |default_path|
88
+ Dir.glob(File.join(RbConfig::CONFIG["rubylibdir"], "*")).each do |default_path|
86
89
  # The default_path might be a Ruby file or a folder with the gem's name. For example:
87
90
  # bundler/
88
91
  # bundler.rb
@@ -103,10 +106,14 @@ module RubyIndexer
103
106
 
104
107
  if pathname.directory?
105
108
  # If the default_path is a directory, we index all the Ruby files in it
106
- files_to_index.concat(Dir.glob("#{default_path}/**/*.rb", File::FNM_PATHNAME | File::FNM_EXTGLOB))
109
+ indexables.concat(
110
+ Dir.glob(File.join(default_path, "**", "*.rb"), File::FNM_PATHNAME | File::FNM_EXTGLOB).map! do |path|
111
+ IndexablePath.new(RbConfig::CONFIG["rubylibdir"], path)
112
+ end,
113
+ )
107
114
  else
108
115
  # If the default_path is a Ruby file, we index it
109
- files_to_index << default_path
116
+ indexables << IndexablePath.new(RbConfig::CONFIG["rubylibdir"], default_path)
110
117
  end
111
118
  end
112
119
 
@@ -121,15 +128,20 @@ module RubyIndexer
121
128
  # duplicates or accidentally ignoring exclude patterns
122
129
  next if spec.full_gem_path == Dir.pwd
123
130
 
124
- files_to_index.concat(Dir.glob("#{spec.full_gem_path}/{#{spec.require_paths.join(",")}}/**/*.rb"))
131
+ indexables.concat(
132
+ spec.require_paths.flat_map do |require_path|
133
+ load_path_entry = File.join(spec.full_gem_path, require_path)
134
+ Dir.glob(File.join(load_path_entry, "**", "*.rb")).map! { |path| IndexablePath.new(load_path_entry, path) }
135
+ end,
136
+ )
125
137
  rescue Gem::MissingSpecError
126
138
  # If a gem is scoped only to some specific platform, then its dependencies may not be installed either, but they
127
139
  # are still listed in locked_gems. We can't index them because they are not installed for the platform, so we
128
140
  # just ignore if they're missing
129
141
  end
130
142
 
131
- files_to_index.uniq!
132
- files_to_index
143
+ indexables.uniq!
144
+ indexables
133
145
  end
134
146
 
135
147
  sig { returns(Regexp) }
@@ -17,35 +17,55 @@ module RubyIndexer
17
17
  # }
18
18
  @entries = T.let({}, T::Hash[String, T::Array[Entry]])
19
19
 
20
+ # Holds all entries in the index using a prefix tree for searching based on prefixes to provide autocompletion
21
+ @entries_tree = T.let(PrefixTree[T::Array[Entry]].new, PrefixTree[T::Array[Entry]])
22
+
20
23
  # Holds references to where entries where discovered so that we can easily delete them
21
24
  # {
22
25
  # "/my/project/foo.rb" => [#<Entry::Class>, #<Entry::Class>],
23
26
  # "/my/project/bar.rb" => [#<Entry::Class>],
24
27
  # }
25
28
  @files_to_entries = T.let({}, T::Hash[String, T::Array[Entry]])
29
+
30
+ # Holds all require paths for every indexed item so that we can provide autocomplete for requires
31
+ @require_paths_tree = T.let(PrefixTree[IndexablePath].new, PrefixTree[IndexablePath])
26
32
  end
27
33
 
28
- sig { params(path: String).void }
29
- def delete(path)
34
+ sig { params(indexable: IndexablePath).void }
35
+ def delete(indexable)
30
36
  # For each constant discovered in `path`, delete the associated entry from the index. If there are no entries
31
37
  # left, delete the constant from the index.
32
- @files_to_entries[path]&.each do |entry|
33
- entries = @entries[entry.name]
38
+ @files_to_entries[indexable.full_path]&.each do |entry|
39
+ name = entry.name
40
+ entries = @entries[name]
34
41
  next unless entries
35
42
 
36
43
  # Delete the specific entry from the list for this name
37
44
  entries.delete(entry)
38
- # If all entries were deleted, then remove the name from the hash
39
- @entries.delete(entry.name) if entries.empty?
45
+
46
+ # If all entries were deleted, then remove the name from the hash and from the prefix tree. Otherwise, update
47
+ # the prefix tree with the current entries
48
+ if entries.empty?
49
+ @entries.delete(name)
50
+ @entries_tree.delete(name)
51
+ else
52
+ @entries_tree.insert(name, entries)
53
+ end
40
54
  end
41
55
 
42
- @files_to_entries.delete(path)
56
+ @files_to_entries.delete(indexable.full_path)
57
+
58
+ require_path = indexable.require_path
59
+ @require_paths_tree.delete(require_path) if require_path
43
60
  end
44
61
 
45
62
  sig { params(entry: Entry).void }
46
63
  def <<(entry)
47
- (@entries[entry.name] ||= []) << entry
64
+ name = entry.name
65
+
66
+ (@entries[name] ||= []) << entry
48
67
  (@files_to_entries[entry.file_path] ||= []) << entry
68
+ @entries_tree.insert(name, T.must(@entries[name]))
49
69
  end
50
70
 
51
71
  sig { params(fully_qualified_name: String).returns(T.nilable(T::Array[Entry])) }
@@ -53,6 +73,36 @@ module RubyIndexer
53
73
  @entries[fully_qualified_name.delete_prefix("::")]
54
74
  end
55
75
 
76
+ sig { params(query: String).returns(T::Array[IndexablePath]) }
77
+ def search_require_paths(query)
78
+ @require_paths_tree.search(query)
79
+ end
80
+
81
+ # Searches entries in the index based on an exact prefix, intended for providing autocomplete. All possible matches
82
+ # to the prefix are returned. The return is an array of arrays, where each entry is the array of entries for a given
83
+ # name match. For example:
84
+ # ## Example
85
+ # ```ruby
86
+ # # If the index has two entries for `Foo::Bar` and one for `Foo::Baz`, then:
87
+ # index.prefix_search("Foo::B")
88
+ # # Will return:
89
+ # [
90
+ # [#<Entry::Class name="Foo::Bar">, #<Entry::Class name="Foo::Bar">],
91
+ # [#<Entry::Class name="Foo::Baz">],
92
+ # ]
93
+ # ```
94
+ sig { params(query: String, nesting: T::Array[String]).returns(T::Array[T::Array[Entry]]) }
95
+ def prefix_search(query, nesting)
96
+ results = (nesting.length + 1).downto(0).flat_map do |i|
97
+ prefix = T.must(nesting[0...i]).join("::")
98
+ namespaced_query = prefix.empty? ? query : "#{prefix}::#{query}"
99
+ @entries_tree.search(namespaced_query)
100
+ end
101
+
102
+ results.uniq!
103
+ results
104
+ end
105
+
56
106
  # Fuzzy searches index entries based on Jaro-Winkler similarity. If no query is provided, all entries are returned
57
107
  sig { params(query: T.nilable(String)).returns(T::Array[Entry]) }
58
108
  def fuzzy_search(query)
@@ -85,16 +135,19 @@ module RubyIndexer
85
135
  nil
86
136
  end
87
137
 
88
- sig { params(paths: T::Array[String]).void }
89
- def index_all(paths: RubyIndexer.configuration.files_to_index)
90
- paths.each { |path| index_single(path) }
138
+ sig { params(indexable_paths: T::Array[IndexablePath]).void }
139
+ def index_all(indexable_paths: RubyIndexer.configuration.indexables)
140
+ indexable_paths.each { |path| index_single(path) }
91
141
  end
92
142
 
93
- sig { params(path: String, source: T.nilable(String)).void }
94
- def index_single(path, source = nil)
95
- content = source || File.read(path)
96
- visitor = IndexVisitor.new(self, YARP.parse(content), path)
143
+ sig { params(indexable_path: IndexablePath, source: T.nilable(String)).void }
144
+ def index_single(indexable_path, source = nil)
145
+ content = source || File.read(indexable_path.full_path)
146
+ visitor = IndexVisitor.new(self, YARP.parse(content), indexable_path.full_path)
97
147
  visitor.run
148
+
149
+ require_path = indexable_path.require_path
150
+ @require_paths_tree.insert(require_path, indexable_path) if require_path
98
151
  rescue Errno::EISDIR
99
152
  # If `path` is a directory, just ignore it and continue indexing
100
153
  end
@@ -0,0 +1,29 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyIndexer
5
+ class IndexablePath
6
+ extend T::Sig
7
+
8
+ sig { returns(T.nilable(String)) }
9
+ attr_reader :require_path
10
+
11
+ sig { returns(String) }
12
+ attr_reader :full_path
13
+
14
+ # An IndexablePath is instantiated with a load_path_entry and a full_path. The load_path_entry is where the file can
15
+ # be found in the $LOAD_PATH, which we use to determine the require_path. The load_path_entry may be `nil` if the
16
+ # indexer is configured to go through files that do not belong in the $LOAD_PATH. For example,
17
+ # `sorbet/tapioca/require.rb` ends up being a part of the paths to be indexed because it's a Ruby file inside the
18
+ # project, but the `sorbet` folder is not a part of the $LOAD_PATH. That means that both its load_path_entry and
19
+ # require_path will be `nil`, since it cannot be required by the project
20
+ sig { params(load_path_entry: T.nilable(String), full_path: String).void }
21
+ def initialize(load_path_entry, full_path)
22
+ @full_path = full_path
23
+ @require_path = T.let(
24
+ load_path_entry ? Pathname.new(full_path).relative_path_from(load_path_entry).to_s.delete_suffix(".rb") : nil,
25
+ T.nilable(String),
26
+ )
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,153 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyIndexer
5
+ # A PrefixTree is a data structure that allows searching for partial strings fast. The tree is similar to a nested
6
+ # hash structure, where the keys are the characters of the inserted strings.
7
+ #
8
+ # ## Example
9
+ # ```ruby
10
+ # tree = PrefixTree[String].new
11
+ # # Insert entries using the same key and value
12
+ # tree.insert("bar", "bar")
13
+ # tree.insert("baz", "baz")
14
+ # # Internally, the structure is analogous to this, but using nodes:
15
+ # # {
16
+ # # "b" => {
17
+ # # "a" => {
18
+ # # "r" => "bar",
19
+ # # "z" => "baz"
20
+ # # }
21
+ # # }
22
+ # # }
23
+ # # When we search it, it finds all possible values based on partial (or complete matches):
24
+ # tree.search("") # => ["bar", "baz"]
25
+ # tree.search("b") # => ["bar", "baz"]
26
+ # tree.search("ba") # => ["bar", "baz"]
27
+ # tree.search("bar") # => ["bar"]
28
+ # ```
29
+ #
30
+ # A PrefixTree is useful for autocomplete, since we always want to find all alternatives while the developer hasn't
31
+ # finished typing yet. This PrefixTree implementation allows for string keys and any arbitrary value using the generic
32
+ # `Value` type.
33
+ #
34
+ # See https://en.wikipedia.org/wiki/Trie for more information
35
+ class PrefixTree
36
+ extend T::Sig
37
+ extend T::Generic
38
+
39
+ Value = type_member
40
+
41
+ sig { void }
42
+ def initialize
43
+ @root = T.let(Node.new("", ""), Node[Value])
44
+ end
45
+
46
+ # Search the PrefixTree based on a given `prefix`. If `foo` is an entry in the tree, then searching for `fo` will
47
+ # return it as a result. The result is always an array of the type of value attribute to the generic `Value` type.
48
+ # Notice that if the `Value` is an array, this method will return an array of arrays, where each entry is the array
49
+ # of values for a given match
50
+ sig { params(prefix: String).returns(T::Array[Value]) }
51
+ def search(prefix)
52
+ node = find_node(prefix)
53
+ return [] unless node
54
+
55
+ node.collect
56
+ end
57
+
58
+ # Inserts a `value` using the given `key`
59
+ sig { params(key: String, value: Value).void }
60
+ def insert(key, value)
61
+ node = @root
62
+
63
+ key.each_char do |char|
64
+ node = node.children[char] ||= Node.new(char, value, node)
65
+ end
66
+
67
+ # This line is to allow a value to be overridden. When we are indexing files, we want to be able to update entries
68
+ # for a given fully qualified name if we find more occurrences of it. Without being able to override, that would
69
+ # not be possible
70
+ node.value = value
71
+ node.leaf = true
72
+ end
73
+
74
+ # Deletes the entry identified by `key` from the tree. Notice that a partial match will still delete all entries
75
+ # that match it. For example, if the tree contains `foo` and we ask to delete `fo`, then `foo` will be deleted
76
+ sig { params(key: String).void }
77
+ def delete(key)
78
+ node = find_node(key)
79
+ return unless node
80
+
81
+ # Remove the node from the tree and then go up the parents to remove any of them with empty children
82
+ parent = T.let(T.must(node.parent), T.nilable(Node[Value]))
83
+
84
+ while parent
85
+ parent.children.delete(node.key)
86
+ return if parent.children.any? || parent.leaf
87
+
88
+ node = parent
89
+ parent = parent.parent
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ # Find a node that matches the given `key`
96
+ sig { params(key: String).returns(T.nilable(Node[Value])) }
97
+ def find_node(key)
98
+ node = @root
99
+
100
+ key.each_char do |char|
101
+ snode = node.children[char]
102
+ return nil unless snode
103
+
104
+ node = snode
105
+ end
106
+
107
+ node
108
+ end
109
+
110
+ class Node
111
+ extend T::Sig
112
+ extend T::Generic
113
+
114
+ Value = type_member
115
+
116
+ sig { returns(T::Hash[String, Node[Value]]) }
117
+ attr_reader :children
118
+
119
+ sig { returns(String) }
120
+ attr_reader :key
121
+
122
+ sig { returns(Value) }
123
+ attr_accessor :value
124
+
125
+ sig { returns(T::Boolean) }
126
+ attr_accessor :leaf
127
+
128
+ sig { returns(T.nilable(Node[Value])) }
129
+ attr_reader :parent
130
+
131
+ sig { params(key: String, value: Value, parent: T.nilable(Node[Value])).void }
132
+ def initialize(key, value, parent = nil)
133
+ @key = key
134
+ @value = value
135
+ @parent = parent
136
+ @children = T.let({}, T::Hash[String, Node[Value]])
137
+ @leaf = T.let(false, T::Boolean)
138
+ end
139
+
140
+ sig { returns(T::Array[Value]) }
141
+ def collect
142
+ result = T.let([], T::Array[Value])
143
+ result << value if leaf
144
+
145
+ children.each_value do |node|
146
+ result.concat(node.collect)
147
+ end
148
+
149
+ result
150
+ end
151
+ end
152
+ end
153
+ end
@@ -4,9 +4,11 @@
4
4
  require "yaml"
5
5
  require "did_you_mean"
6
6
 
7
+ require "ruby_indexer/lib/ruby_indexer/indexable_path"
7
8
  require "ruby_indexer/lib/ruby_indexer/visitor"
8
9
  require "ruby_indexer/lib/ruby_indexer/index"
9
10
  require "ruby_indexer/lib/ruby_indexer/configuration"
11
+ require "ruby_indexer/lib/ruby_indexer/prefix_tree"
10
12
 
11
13
  module RubyIndexer
12
14
  class << self
@@ -139,7 +139,7 @@ module RubyIndexer
139
139
 
140
140
  assert_entry("Foo", Index::Entry::Class, "/fake/path/foo.rb:0-0:1-2")
141
141
 
142
- @index.delete("/fake/path/foo.rb")
142
+ @index.delete(IndexablePath.new(nil, "/fake/path/foo.rb"))
143
143
  refute_entry("Foo")
144
144
  assert_empty(@index.instance_variable_get(:@files_to_entries))
145
145
  end
@@ -11,55 +11,57 @@ module RubyIndexer
11
11
 
12
12
  def test_load_configuration_executes_configure_block
13
13
  @config.load_config
14
- files_to_index = @config.files_to_index
14
+ indexables = @config.indexables
15
15
 
16
- assert(files_to_index.none? { |path| path.include?("test/fixtures") })
17
- assert(files_to_index.none? { |path| path.include?("minitest-reporters") })
18
- assert(files_to_index.none? { |path| path == __FILE__ })
16
+ assert(indexables.none? { |indexable| indexable.full_path.include?("test/fixtures") })
17
+ assert(indexables.none? { |indexable| indexable.full_path.include?("minitest-reporters") })
18
+ assert(indexables.none? { |indexable| indexable.full_path == __FILE__ })
19
19
  end
20
20
 
21
- def test_files_to_index_only_includes_gem_require_paths
21
+ def test_indexables_only_includes_gem_require_paths
22
22
  @config.load_config
23
- files_to_index = @config.files_to_index
23
+ indexables = @config.indexables
24
24
 
25
25
  Bundler.locked_gems.specs.each do |lazy_spec|
26
26
  next if lazy_spec.name == "ruby-lsp"
27
27
 
28
28
  spec = Gem::Specification.find_by_name(lazy_spec.name)
29
- assert(files_to_index.none? { |path| path.start_with?("#{spec.full_gem_path}/test/") })
29
+ assert(indexables.none? { |indexable| indexable.full_path.start_with?("#{spec.full_gem_path}/test/") })
30
30
  rescue Gem::MissingSpecError
31
31
  # Transitive dependencies might be missing when running tests on Windows
32
32
  end
33
33
  end
34
34
 
35
- def test_files_to_index_does_not_include_default_gem_path_when_in_bundle
35
+ def test_indexables_does_not_include_default_gem_path_when_in_bundle
36
36
  @config.load_config
37
- files_to_index = @config.files_to_index
37
+ indexables = @config.indexables
38
38
 
39
- assert(files_to_index.none? { |path| path.start_with?("#{RbConfig::CONFIG["rubylibdir"]}/psych") })
39
+ assert(
40
+ indexables.none? { |indexable| indexable.full_path.start_with?("#{RbConfig::CONFIG["rubylibdir"]}/psych") },
41
+ )
40
42
  end
41
43
 
42
- def test_files_to_index_includes_default_gems
44
+ def test_indexables_includes_default_gems
43
45
  @config.load_config
44
- files_to_index = @config.files_to_index
46
+ indexables = @config.indexables.map(&:full_path)
45
47
 
46
- assert_includes(files_to_index, "#{RbConfig::CONFIG["rubylibdir"]}/pathname.rb")
47
- assert_includes(files_to_index, "#{RbConfig::CONFIG["rubylibdir"]}/ipaddr.rb")
48
- assert_includes(files_to_index, "#{RbConfig::CONFIG["rubylibdir"]}/abbrev.rb")
48
+ assert_includes(indexables, "#{RbConfig::CONFIG["rubylibdir"]}/pathname.rb")
49
+ assert_includes(indexables, "#{RbConfig::CONFIG["rubylibdir"]}/ipaddr.rb")
50
+ assert_includes(indexables, "#{RbConfig::CONFIG["rubylibdir"]}/abbrev.rb")
49
51
  end
50
52
 
51
- def test_files_to_index_includes_project_files
53
+ def test_indexables_includes_project_files
52
54
  @config.load_config
53
- files_to_index = @config.files_to_index
55
+ indexables = @config.indexables.map(&:full_path)
54
56
 
55
57
  Dir.glob("#{Dir.pwd}/lib/**/*.rb").each do |path|
56
58
  next if path.end_with?("_test.rb")
57
59
 
58
- assert_includes(files_to_index, path)
60
+ assert_includes(indexables, path)
59
61
  end
60
62
  end
61
63
 
62
- def test_files_to_index_avoids_duplicates_if_bundle_path_is_inside_project
64
+ def test_indexables_avoids_duplicates_if_bundle_path_is_inside_project
63
65
  Bundler.settings.set_global("path", "vendor/bundle")
64
66
  config = Configuration.new
65
67
  config.load_config
@@ -69,18 +71,22 @@ module RubyIndexer
69
71
  Bundler.settings.set_global("path", nil)
70
72
  end
71
73
 
72
- def test_files_to_index_does_not_include_gems_own_installed_files
74
+ def test_indexables_does_not_include_gems_own_installed_files
73
75
  @config.load_config
74
- files_to_index = @config.files_to_index
76
+ indexables = @config.indexables
75
77
 
76
- assert(files_to_index.none? { |path| path.start_with?(Bundler.bundle_path.join("gems", "ruby-lsp").to_s) })
78
+ assert(
79
+ indexables.none? do |indexable|
80
+ indexable.full_path.start_with?(Bundler.bundle_path.join("gems", "ruby-lsp").to_s)
81
+ end,
82
+ )
77
83
  end
78
84
 
79
85
  def test_paths_are_unique
80
86
  @config.load_config
81
- files_to_index = @config.files_to_index
87
+ indexables = @config.indexables
82
88
 
83
- assert_equal(files_to_index.uniq.length, files_to_index.length)
89
+ assert_equal(indexables.uniq.length, indexables.length)
84
90
  end
85
91
 
86
92
  def test_configuration_raises_for_unknown_keys
@@ -6,11 +6,11 @@ require_relative "test_case"
6
6
  module RubyIndexer
7
7
  class IndexTest < TestCase
8
8
  def test_deleting_one_entry_for_a_class
9
- @index.index_single("/fake/path/foo.rb", <<~RUBY)
9
+ @index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY)
10
10
  class Foo
11
11
  end
12
12
  RUBY
13
- @index.index_single("/fake/path/other_foo.rb", <<~RUBY)
13
+ @index.index_single(IndexablePath.new(nil, "/fake/path/other_foo.rb"), <<~RUBY)
14
14
  class Foo
15
15
  end
16
16
  RUBY
@@ -18,13 +18,13 @@ module RubyIndexer
18
18
  entries = @index["Foo"]
19
19
  assert_equal(2, entries.length)
20
20
 
21
- @index.delete("/fake/path/other_foo.rb")
21
+ @index.delete(IndexablePath.new(nil, "/fake/path/other_foo.rb"))
22
22
  entries = @index["Foo"]
23
23
  assert_equal(1, entries.length)
24
24
  end
25
25
 
26
26
  def test_deleting_all_entries_for_a_class
27
- @index.index_single("/fake/path/foo.rb", <<~RUBY)
27
+ @index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY)
28
28
  class Foo
29
29
  end
30
30
  RUBY
@@ -32,13 +32,13 @@ module RubyIndexer
32
32
  entries = @index["Foo"]
33
33
  assert_equal(1, entries.length)
34
34
 
35
- @index.delete("/fake/path/foo.rb")
35
+ @index.delete(IndexablePath.new(nil, "/fake/path/foo.rb"))
36
36
  entries = @index["Foo"]
37
37
  assert_nil(entries)
38
38
  end
39
39
 
40
40
  def test_index_resolve
41
- @index.index_single("/fake/path/foo.rb", <<~RUBY)
41
+ @index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY)
42
42
  class Bar; end
43
43
 
44
44
  module Foo
@@ -72,7 +72,7 @@ module RubyIndexer
72
72
  end
73
73
 
74
74
  def test_accessing_with_colon_colon_prefix
75
- @index.index_single("/fake/path/foo.rb", <<~RUBY)
75
+ @index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY)
76
76
  class Bar; end
77
77
 
78
78
  module Foo
@@ -92,7 +92,7 @@ module RubyIndexer
92
92
  end
93
93
 
94
94
  def test_fuzzy_search
95
- @index.index_single("/fake/path/foo.rb", <<~RUBY)
95
+ @index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb"), <<~RUBY)
96
96
  class Bar; end
97
97
 
98
98
  module Foo
@@ -121,9 +121,42 @@ module RubyIndexer
121
121
 
122
122
  def test_index_single_ignores_directories
123
123
  FileUtils.mkdir("lib/this_is_a_dir.rb")
124
- @index.index_single("lib/this_is_a_dir.rb")
124
+ @index.index_single(IndexablePath.new(nil, "lib/this_is_a_dir.rb"))
125
125
  ensure
126
126
  FileUtils.rm_r("lib/this_is_a_dir.rb")
127
127
  end
128
+
129
+ def test_searching_for_require_paths
130
+ @index.index_single(IndexablePath.new("/fake", "/fake/path/foo.rb"), <<~RUBY)
131
+ class Foo
132
+ end
133
+ RUBY
134
+ @index.index_single(IndexablePath.new("/fake", "/fake/path/other_foo.rb"), <<~RUBY)
135
+ class Foo
136
+ end
137
+ RUBY
138
+
139
+ assert_equal(["path/foo", "path/other_foo"], @index.search_require_paths("path").map(&:require_path))
140
+ end
141
+
142
+ def test_searching_for_entries_based_on_prefix
143
+ @index.index_single(IndexablePath.new("/fake", "/fake/path/foo.rb"), <<~RUBY)
144
+ class Foo::Bar
145
+ end
146
+ RUBY
147
+ @index.index_single(IndexablePath.new("/fake", "/fake/path/other_foo.rb"), <<~RUBY)
148
+ class Foo::Bar
149
+ end
150
+
151
+ class Foo::Baz
152
+ end
153
+ RUBY
154
+
155
+ results = @index.prefix_search("Foo", []).map { |entries| entries.map(&:name) }
156
+ assert_equal([["Foo::Bar", "Foo::Bar"], ["Foo::Baz"]], results)
157
+
158
+ results = @index.prefix_search("Ba", ["Foo"]).map { |entries| entries.map(&:name) }
159
+ assert_equal([["Foo::Bar", "Foo::Bar"], ["Foo::Baz"]], results)
160
+ end
128
161
  end
129
162
  end