ruby-lsp 0.9.3 → 0.9.4

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: 5a300c17567fae60c2ed50c941d8ada19bf6b6fd951b621029880c03afd6d6f5
4
+ data.tar.gz: 297d0d2064fbe7e97783e26fbfbdbb2191b6a32a43636e743864ad08665f49dd
5
5
  SHA512:
6
- metadata.gz: 184683630988346ae617e3df3d6d04c66b822d6d9734f595a41caa3603b38046e5ee5a7a4361abede0f3b8d1584371730be9af905d006fa881d4b718f3381947
7
- data.tar.gz: 7c10ef841c059110b1d5bc38cde57de243140c37d7095ab8c7ce16d28e4c520c402f2bfcf604f7002a7e06d7db031b8ab9e52ce28bf3c50be0dc2671d5728170
6
+ metadata.gz: ee884d5c7f60e1d3fac9069a170967a761407a6470b7fe7442686bdbdfc1a4cbd626b7bcd4d368072bce0bab349df935bc0e7e9bdcad7b2fa238143afbc78075
7
+ data.tar.gz: 317ebe547e3a9730c0161a2ad8b0dd130e819bf92a97c59525e5bc40e1e84bc95e0e610f87f144aebef957a622e141254938954bafd354e7f3d1249e022d2edb
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.9.3
1
+ 0.9.4
@@ -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[String].new, PrefixTree[String])
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[String]) }
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, require_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"))
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
@@ -0,0 +1,150 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "test_helper"
5
+
6
+ module RubyIndexer
7
+ class PrefixTreeTest < Minitest::Test
8
+ def test_empty
9
+ tree = PrefixTree.new
10
+
11
+ assert_empty(tree.search(""))
12
+ assert_empty(tree.search("foo"))
13
+ end
14
+
15
+ def test_single_item
16
+ tree = PrefixTree.new
17
+ tree.insert("foo", "foo")
18
+
19
+ assert_equal(["foo"], tree.search(""))
20
+ assert_equal(["foo"], tree.search("foo"))
21
+ assert_empty(tree.search("bar"))
22
+ end
23
+
24
+ def test_multiple_items
25
+ tree = PrefixTree[String].new
26
+ ["foo", "bar", "baz"].each { |item| tree.insert(item, item) }
27
+
28
+ assert_equal(["foo", "bar", "baz"], tree.search(""))
29
+ assert_equal(["bar", "baz"], tree.search("b"))
30
+ assert_equal(["foo"], tree.search("fo"))
31
+ assert_equal(["bar", "baz"], tree.search("ba"))
32
+ assert_equal(["baz"], tree.search("baz"))
33
+ assert_empty(tree.search("qux"))
34
+ end
35
+
36
+ def test_multiple_prefixes
37
+ tree = PrefixTree[String].new
38
+ ["fo", "foo"].each { |item| tree.insert(item, item) }
39
+
40
+ assert_equal(["fo", "foo"], tree.search(""))
41
+ assert_equal(["fo", "foo"], tree.search("f"))
42
+ assert_equal(["fo", "foo"], tree.search("fo"))
43
+ assert_equal(["foo"], tree.search("foo"))
44
+ assert_empty(tree.search("fooo"))
45
+ end
46
+
47
+ def test_multiple_prefixes_with_shuffled_order
48
+ tree = PrefixTree[String].new
49
+ [
50
+ "foo/bar/base",
51
+ "foo/bar/on",
52
+ "foo/bar/support/selection",
53
+ "foo/bar/support/runner",
54
+ "foo/internal",
55
+ "foo/bar/document",
56
+ "foo/bar/code",
57
+ "foo/bar/support/rails",
58
+ "foo/bar/diagnostics",
59
+ "foo/bar/document2",
60
+ "foo/bar/support/runner2",
61
+ "foo/bar/support/diagnostic",
62
+ "foo/document",
63
+ "foo/bar/formatting",
64
+ "foo/bar/support/highlight",
65
+ "foo/bar/semantic",
66
+ "foo/bar/support/prefix",
67
+ "foo/bar/folding",
68
+ "foo/bar/selection",
69
+ "foo/bar/support/syntax",
70
+ "foo/bar/document3",
71
+ "foo/bar/hover",
72
+ "foo/bar/support/semantic",
73
+ "foo/bar/support/source",
74
+ "foo/bar/inlay",
75
+ "foo/requests",
76
+ "foo/bar/support/formatting",
77
+ "foo/bar/path",
78
+ "foo/executor",
79
+ ].each { |item| tree.insert(item, item) }
80
+
81
+ assert_equal(
82
+ [
83
+ "foo/bar/support/selection",
84
+ "foo/bar/support/semantic",
85
+ "foo/bar/support/syntax",
86
+ "foo/bar/support/source",
87
+ "foo/bar/support/runner",
88
+ "foo/bar/support/runner2",
89
+ "foo/bar/support/rails",
90
+ "foo/bar/support/diagnostic",
91
+ "foo/bar/support/highlight",
92
+ "foo/bar/support/prefix",
93
+ "foo/bar/support/formatting",
94
+ ],
95
+ tree.search("foo/bar/support"),
96
+ )
97
+ end
98
+
99
+ def test_deletion
100
+ tree = PrefixTree[String].new
101
+ ["foo/bar", "foo/baz"].each { |item| tree.insert(item, item) }
102
+ assert_equal(["foo/bar", "foo/baz"], tree.search("foo"))
103
+
104
+ tree.delete("foo/bar")
105
+ assert_empty(tree.search("foo/bar"))
106
+ assert_equal(["foo/baz"], tree.search("foo"))
107
+ end
108
+
109
+ def test_delete_does_not_impact_other_keys_with_the_same_value
110
+ tree = PrefixTree[String].new
111
+ tree.insert("key1", "value")
112
+ tree.insert("key2", "value")
113
+ assert_equal(["value", "value"], tree.search("key"))
114
+
115
+ tree.delete("key2")
116
+ assert_empty(tree.search("key2"))
117
+ assert_equal(["value"], tree.search("key1"))
118
+ end
119
+
120
+ def test_deleted_node_is_removed_from_the_tree
121
+ tree = PrefixTree[String].new
122
+ tree.insert("foo/bar", "foo/bar")
123
+ assert_equal(["foo/bar"], tree.search("foo"))
124
+
125
+ tree.delete("foo/bar")
126
+ root = tree.instance_variable_get(:@root)
127
+ assert_empty(root.children)
128
+ end
129
+
130
+ def test_deleting_non_terminal_nodes
131
+ tree = PrefixTree[String].new
132
+ tree.insert("abc", "value1")
133
+ tree.insert("abcdef", "value2")
134
+
135
+ tree.delete("abcdef")
136
+ assert_empty(tree.search("abcdef"))
137
+ assert_equal(["value1"], tree.search("abc"))
138
+ end
139
+
140
+ def test_overriding_values
141
+ tree = PrefixTree[Integer].new
142
+
143
+ tree.insert("foo/bar", 123)
144
+ assert_equal([123], tree.search("foo/bar"))
145
+
146
+ tree.insert("foo/bar", 456)
147
+ assert_equal([456], tree.search("foo/bar"))
148
+ end
149
+ end
150
+ end
@@ -12,7 +12,7 @@ module RubyIndexer
12
12
  private
13
13
 
14
14
  def index(source)
15
- @index.index_single("/fake/path/foo.rb", source)
15
+ @index.index_single(IndexablePath.new(nil, "/fake/path/foo.rb"), source)
16
16
  end
17
17
 
18
18
  def assert_entry(expected_name, type, expected_location)
@@ -191,14 +191,17 @@ module RubyLsp
191
191
  file_path = uri.to_standardized_path
192
192
  next if file_path.nil? || File.directory?(file_path)
193
193
 
194
+ load_path_entry = $LOAD_PATH.find { |load_path| file_path.start_with?(load_path) }
195
+ indexable = RubyIndexer::IndexablePath.new(load_path_entry, file_path)
196
+
194
197
  case change[:type]
195
198
  when Constant::FileChangeType::CREATED
196
- @index.index_single(file_path)
199
+ @index.index_single(indexable)
197
200
  when Constant::FileChangeType::CHANGED
198
- @index.delete(file_path)
199
- @index.index_single(file_path)
201
+ @index.delete(indexable)
202
+ @index.index_single(indexable)
200
203
  when Constant::FileChangeType::DELETED
201
- @index.delete(file_path)
204
+ @index.delete(indexable)
202
205
  end
203
206
  end
204
207
 
@@ -300,7 +303,9 @@ module RubyLsp
300
303
  hover.response
301
304
  end
302
305
 
303
- sig { params(uri: URI::Generic, content_changes: T::Array[Document::EditShape], version: Integer).returns(Object) }
306
+ sig do
307
+ params(uri: URI::Generic, content_changes: T::Array[Document::EditShape], version: Integer).returns(Object)
308
+ end
304
309
  def text_document_did_change(uri, content_changes, version)
305
310
  @store.push_edits(uri: uri, edits: content_changes, version: version)
306
311
  VOID
@@ -504,7 +509,7 @@ module RubyLsp
504
509
  return unless target
505
510
 
506
511
  emitter = EventEmitter.new
507
- listener = Requests::PathCompletion.new(emitter, @message_queue)
512
+ listener = Requests::PathCompletion.new(@index, emitter, @message_queue)
508
513
  emitter.emit_for_target(target)
509
514
  listener.response
510
515
  end
@@ -130,7 +130,7 @@ module RubyLsp
130
130
  overridable.params(
131
131
  emitter: EventEmitter,
132
132
  message_queue: Thread::Queue,
133
- ).returns(T.nilable(Listener[T.nilable(Interface::DocumentSymbol)]))
133
+ ).returns(T.nilable(Listener[T::Array[Interface::DocumentSymbol]]))
134
134
  end
135
135
  def create_document_symbol_listener(emitter, message_queue); end
136
136
  end
@@ -22,33 +22,24 @@ module RubyLsp
22
22
  sig { override.returns(ResponseType) }
23
23
  attr_reader :response
24
24
 
25
- sig { params(emitter: EventEmitter, message_queue: Thread::Queue).void }
26
- def initialize(emitter, message_queue)
27
- super
25
+ sig { params(index: RubyIndexer::Index, emitter: EventEmitter, message_queue: Thread::Queue).void }
26
+ def initialize(index, emitter, message_queue)
27
+ super(emitter, message_queue)
28
28
  @response = T.let([], ResponseType)
29
- @tree = T.let(Support::PrefixTree.new(collect_load_path_files), Support::PrefixTree)
29
+ @index = index
30
30
 
31
31
  emitter.register(self, :on_tstring_content)
32
32
  end
33
33
 
34
34
  sig { params(node: SyntaxTree::TStringContent).void }
35
35
  def on_tstring_content(node)
36
- @tree.search(node.value).sort.each do |path|
36
+ @index.search_require_paths(node.value).sort!.each do |path|
37
37
  @response << build_completion(path, node)
38
38
  end
39
39
  end
40
40
 
41
41
  private
42
42
 
43
- sig { returns(T::Array[String]) }
44
- def collect_load_path_files
45
- $LOAD_PATH.flat_map do |p|
46
- Dir.glob("**/*.rb", base: p)
47
- end.map! do |result|
48
- result.delete_suffix!(".rb")
49
- end
50
- end
51
-
52
43
  sig { params(label: String, node: SyntaxTree::TStringContent).returns(Interface::CompletionItem) }
53
44
  def build_completion(label, node)
54
45
  Interface::CompletionItem.new(
@@ -53,7 +53,6 @@ module RubyLsp
53
53
  autoload :Sorbet, "ruby_lsp/requests/support/sorbet"
54
54
  autoload :HighlightTarget, "ruby_lsp/requests/support/highlight_target"
55
55
  autoload :RailsDocumentClient, "ruby_lsp/requests/support/rails_document_client"
56
- autoload :PrefixTree, "ruby_lsp/requests/support/prefix_tree"
57
56
  autoload :Common, "ruby_lsp/requests/support/common"
58
57
  autoload :FormatterRunner, "ruby_lsp/requests/support/formatter_runner"
59
58
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-lsp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.3
4
+ version: 0.9.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shopify
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-09-01 00:00:00.000000000 Z
11
+ date: 2023-09-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: language_server-protocol
@@ -67,7 +67,7 @@ dependencies:
67
67
  version: '0.9'
68
68
  - - "<"
69
69
  - !ruby/object:Gem::Version
70
- version: '0.10'
70
+ version: '0.11'
71
71
  type: :runtime
72
72
  prerelease: false
73
73
  version_requirements: !ruby/object:Gem::Requirement
@@ -77,7 +77,7 @@ dependencies:
77
77
  version: '0.9'
78
78
  - - "<"
79
79
  - !ruby/object:Gem::Version
80
- version: '0.10'
80
+ version: '0.11'
81
81
  description: An opinionated language server for Ruby
82
82
  email:
83
83
  - ruby@shopify.com
@@ -97,12 +97,15 @@ files:
97
97
  - lib/ruby-lsp.rb
98
98
  - lib/ruby_indexer/lib/ruby_indexer/configuration.rb
99
99
  - lib/ruby_indexer/lib/ruby_indexer/index.rb
100
+ - lib/ruby_indexer/lib/ruby_indexer/indexable_path.rb
101
+ - lib/ruby_indexer/lib/ruby_indexer/prefix_tree.rb
100
102
  - lib/ruby_indexer/lib/ruby_indexer/visitor.rb
101
103
  - lib/ruby_indexer/ruby_indexer.rb
102
104
  - lib/ruby_indexer/test/classes_and_modules_test.rb
103
105
  - lib/ruby_indexer/test/configuration_test.rb
104
106
  - lib/ruby_indexer/test/constant_test.rb
105
107
  - lib/ruby_indexer/test/index_test.rb
108
+ - lib/ruby_indexer/test/prefix_tree_test.rb
106
109
  - lib/ruby_indexer/test/test_case.rb
107
110
  - lib/ruby_lsp/check_docs.rb
108
111
  - lib/ruby_lsp/document.rb
@@ -135,7 +138,6 @@ files:
135
138
  - lib/ruby_lsp/requests/support/dependency_detector.rb
136
139
  - lib/ruby_lsp/requests/support/formatter_runner.rb
137
140
  - lib/ruby_lsp/requests/support/highlight_target.rb
138
- - lib/ruby_lsp/requests/support/prefix_tree.rb
139
141
  - lib/ruby_lsp/requests/support/rubocop_diagnostic.rb
140
142
  - lib/ruby_lsp/requests/support/rubocop_diagnostics_runner.rb
141
143
  - lib/ruby_lsp/requests/support/rubocop_formatting_runner.rb
@@ -170,7 +172,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
170
172
  - !ruby/object:Gem::Version
171
173
  version: '0'
172
174
  requirements: []
173
- rubygems_version: 3.4.18
175
+ rubygems_version: 3.4.19
174
176
  signing_key:
175
177
  specification_version: 4
176
178
  summary: An opinionated language server for Ruby
@@ -1,80 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- module RubyLsp
5
- module Requests
6
- module Support
7
- class PrefixTree
8
- extend T::Sig
9
-
10
- sig { params(items: T::Array[String]).void }
11
- def initialize(items)
12
- @root = T.let(Node.new(""), Node)
13
-
14
- items.each do |item|
15
- insert(item)
16
- end
17
- end
18
-
19
- sig { params(prefix: String).returns(T::Array[String]) }
20
- def search(prefix)
21
- node = T.let(@root, Node)
22
-
23
- prefix.each_char do |char|
24
- snode = node.children[char]
25
- return [] unless snode
26
-
27
- node = snode
28
- end
29
-
30
- node.collect
31
- end
32
-
33
- private
34
-
35
- sig { params(item: String).void }
36
- def insert(item)
37
- node = T.let(@root, Node)
38
-
39
- item.each_char do |char|
40
- node = node.children[char] ||= Node.new(node.value + char)
41
- end
42
-
43
- node.leaf = true
44
- end
45
-
46
- class Node
47
- extend T::Sig
48
-
49
- sig { returns(T::Hash[String, Node]) }
50
- attr_reader :children
51
-
52
- sig { returns(String) }
53
- attr_reader :value
54
-
55
- sig { returns(T::Boolean) }
56
- attr_accessor :leaf
57
-
58
- sig { params(value: String).void }
59
- def initialize(value)
60
- @children = T.let({}, T::Hash[String, Node])
61
- @value = T.let(value, String)
62
- @leaf = T.let(false, T::Boolean)
63
- end
64
-
65
- sig { returns(T::Array[String]) }
66
- def collect
67
- result = T.let([], T::Array[String])
68
- result << value if leaf
69
-
70
- children.each_value do |node|
71
- result.concat(node.collect)
72
- end
73
-
74
- result
75
- end
76
- end
77
- end
78
- end
79
- end
80
- end