ruby-lsp 0.13.1 → 0.13.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +30 -0
  3. data/VERSION +1 -1
  4. data/lib/ruby_indexer/lib/ruby_indexer/configuration.rb +1 -1
  5. data/lib/ruby_indexer/lib/ruby_indexer/entry.rb +30 -1
  6. data/lib/ruby_lsp/check_docs.rb +3 -3
  7. data/lib/ruby_lsp/document.rb +15 -7
  8. data/lib/ruby_lsp/executor.rb +85 -226
  9. data/lib/ruby_lsp/listener.rb +1 -50
  10. data/lib/ruby_lsp/listeners/code_lens.rb +233 -0
  11. data/lib/ruby_lsp/listeners/completion.rb +275 -0
  12. data/lib/ruby_lsp/listeners/definition.rb +158 -0
  13. data/lib/ruby_lsp/listeners/document_highlight.rb +556 -0
  14. data/lib/ruby_lsp/listeners/document_link.rb +162 -0
  15. data/lib/ruby_lsp/listeners/document_symbol.rb +223 -0
  16. data/lib/ruby_lsp/listeners/folding_ranges.rb +271 -0
  17. data/lib/ruby_lsp/listeners/hover.rb +152 -0
  18. data/lib/ruby_lsp/listeners/inlay_hints.rb +80 -0
  19. data/lib/ruby_lsp/listeners/semantic_highlighting.rb +430 -0
  20. data/lib/ruby_lsp/listeners/signature_help.rb +74 -0
  21. data/lib/ruby_lsp/requests/code_action_resolve.rb +5 -5
  22. data/lib/ruby_lsp/requests/code_actions.rb +15 -6
  23. data/lib/ruby_lsp/requests/code_lens.rb +21 -221
  24. data/lib/ruby_lsp/requests/completion.rb +64 -246
  25. data/lib/ruby_lsp/requests/definition.rb +34 -147
  26. data/lib/ruby_lsp/requests/diagnostics.rb +17 -5
  27. data/lib/ruby_lsp/requests/document_highlight.rb +12 -536
  28. data/lib/ruby_lsp/requests/document_link.rb +11 -132
  29. data/lib/ruby_lsp/requests/document_symbol.rb +23 -210
  30. data/lib/ruby_lsp/requests/folding_ranges.rb +16 -252
  31. data/lib/ruby_lsp/requests/formatting.rb +4 -4
  32. data/lib/ruby_lsp/requests/hover.rb +48 -92
  33. data/lib/ruby_lsp/requests/inlay_hints.rb +23 -56
  34. data/lib/ruby_lsp/requests/on_type_formatting.rb +18 -6
  35. data/lib/ruby_lsp/requests/request.rb +17 -0
  36. data/lib/ruby_lsp/requests/selection_ranges.rb +4 -3
  37. data/lib/ruby_lsp/requests/semantic_highlighting.rb +21 -408
  38. data/lib/ruby_lsp/requests/show_syntax_tree.rb +5 -5
  39. data/lib/ruby_lsp/requests/signature_help.rb +87 -0
  40. data/lib/ruby_lsp/requests/support/common.rb +3 -2
  41. data/lib/ruby_lsp/requests/support/dependency_detector.rb +2 -0
  42. data/lib/ruby_lsp/requests/support/selection_range.rb +1 -1
  43. data/lib/ruby_lsp/requests/support/semantic_token_encoder.rb +2 -2
  44. data/lib/ruby_lsp/requests/workspace_symbol.rb +5 -4
  45. data/lib/ruby_lsp/requests.rb +3 -1
  46. data/lib/ruby_lsp/store.rb +1 -1
  47. data/lib/ruby_lsp/utils.rb +8 -0
  48. metadata +20 -8
  49. data/lib/ruby_lsp/requests/base_request.rb +0 -24
@@ -16,6 +16,7 @@ module RubyLsp
16
16
 
17
17
  sig { params(dispatcher: Prism::Dispatcher).void }
18
18
  def initialize(dispatcher)
19
+ super()
19
20
  @dispatcher = dispatcher
20
21
  end
21
22
 
@@ -29,54 +30,4 @@ module RubyLsp
29
30
  sig { abstract.returns(ResponseType) }
30
31
  def _response; end
31
32
  end
32
-
33
- # ExtensibleListener is an abstract class to be used by requests that accept addons.
34
- class ExtensibleListener < Listener
35
- extend T::Sig
36
- extend T::Generic
37
-
38
- ResponseType = type_member
39
-
40
- abstract!
41
-
42
- # When inheriting from ExtensibleListener, the `super` of constructor must be called **after** the subclass's own
43
- # ivars have been initialized. This is because the constructor of ExtensibleListener calls
44
- # `initialize_external_listener` which may depend on the subclass's ivars.
45
- sig { params(dispatcher: Prism::Dispatcher).void }
46
- def initialize(dispatcher)
47
- super
48
- @response_merged = T.let(false, T::Boolean)
49
- @external_listeners = T.let(
50
- Addon.addons.filter_map do |ext|
51
- initialize_external_listener(ext)
52
- end,
53
- T::Array[RubyLsp::Listener[ResponseType]],
54
- )
55
- end
56
-
57
- # Merge responses from all external listeners into the base listener's response. We do this to return a single
58
- # response to the editor including the results of all addons
59
- sig { void }
60
- def merge_external_listeners_responses!
61
- @external_listeners.each { |l| merge_response!(l) }
62
- end
63
-
64
- sig { returns(ResponseType) }
65
- def response
66
- merge_external_listeners_responses! unless @response_merged
67
- super
68
- end
69
-
70
- sig do
71
- abstract.params(addon: RubyLsp::Addon).returns(T.nilable(RubyLsp::Listener[ResponseType]))
72
- end
73
- def initialize_external_listener(addon); end
74
-
75
- # Does nothing by default. Requests that accept addons should override this method to define how to merge responses
76
- # coming from external listeners
77
- sig { abstract.params(other: Listener[T.untyped]).returns(T.self_type) }
78
- def merge_response!(other)
79
- end
80
- end
81
- private_constant(:ExtensibleListener)
82
33
  end
@@ -0,0 +1,233 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "shellwords"
5
+ require_relative "../listener"
6
+
7
+ module RubyLsp
8
+ module Listeners
9
+ class CodeLens < Listener
10
+ extend T::Sig
11
+ extend T::Generic
12
+
13
+ BASE_COMMAND = T.let(
14
+ begin
15
+ Bundler.with_original_env { Bundler.default_lockfile }
16
+ "bundle exec ruby"
17
+ rescue Bundler::GemfileNotFound
18
+ "ruby"
19
+ end + " -Itest ",
20
+ String,
21
+ )
22
+ ACCESS_MODIFIERS = T.let([:public, :private, :protected], T::Array[Symbol])
23
+ SUPPORTED_TEST_LIBRARIES = T.let(["minitest", "test-unit"], T::Array[String])
24
+
25
+ ResponseType = type_member { { fixed: T::Array[Interface::CodeLens] } }
26
+
27
+ sig { override.returns(ResponseType) }
28
+ attr_reader :_response
29
+
30
+ sig do
31
+ params(
32
+ uri: URI::Generic,
33
+ lenses_configuration: RequestConfig,
34
+ dispatcher: Prism::Dispatcher,
35
+ ).void
36
+ end
37
+ def initialize(uri, lenses_configuration, dispatcher)
38
+ @uri = T.let(uri, URI::Generic)
39
+ @_response = T.let([], ResponseType)
40
+ @path = T.let(uri.to_standardized_path, T.nilable(String))
41
+ # visibility_stack is a stack of [current_visibility, previous_visibility]
42
+ @visibility_stack = T.let([[:public, :public]], T::Array[T::Array[T.nilable(Symbol)]])
43
+ @class_stack = T.let([], T::Array[String])
44
+ @group_id = T.let(1, Integer)
45
+ @group_id_stack = T.let([], T::Array[Integer])
46
+ @lenses_configuration = lenses_configuration
47
+
48
+ super(dispatcher)
49
+
50
+ dispatcher.register(
51
+ self,
52
+ :on_class_node_enter,
53
+ :on_class_node_leave,
54
+ :on_def_node_enter,
55
+ :on_call_node_enter,
56
+ :on_call_node_leave,
57
+ )
58
+ end
59
+
60
+ sig { params(node: Prism::ClassNode).void }
61
+ def on_class_node_enter(node)
62
+ @visibility_stack.push([:public, :public])
63
+ class_name = node.constant_path.slice
64
+ @class_stack.push(class_name)
65
+
66
+ if @path && class_name.end_with?("Test")
67
+ add_test_code_lens(
68
+ node,
69
+ name: class_name,
70
+ command: generate_test_command(class_name: class_name),
71
+ kind: :group,
72
+ )
73
+ end
74
+
75
+ @group_id_stack.push(@group_id)
76
+ @group_id += 1
77
+ end
78
+
79
+ sig { params(node: Prism::ClassNode).void }
80
+ def on_class_node_leave(node)
81
+ @visibility_stack.pop
82
+ @class_stack.pop
83
+ @group_id_stack.pop
84
+ end
85
+
86
+ sig { params(node: Prism::DefNode).void }
87
+ def on_def_node_enter(node)
88
+ class_name = @class_stack.last
89
+ return unless class_name&.end_with?("Test")
90
+
91
+ visibility, _ = @visibility_stack.last
92
+ if visibility == :public
93
+ method_name = node.name.to_s
94
+ if @path && method_name.start_with?("test_")
95
+ add_test_code_lens(
96
+ node,
97
+ name: method_name,
98
+ command: generate_test_command(method_name: method_name, class_name: class_name),
99
+ kind: :example,
100
+ )
101
+ end
102
+ end
103
+ end
104
+
105
+ sig { params(node: Prism::CallNode).void }
106
+ def on_call_node_enter(node)
107
+ name = node.name
108
+ arguments = node.arguments
109
+
110
+ # If we found `private` by itself or `private def foo`
111
+ if ACCESS_MODIFIERS.include?(name)
112
+ if arguments.nil?
113
+ @visibility_stack.pop
114
+ @visibility_stack.push([name, name])
115
+ elsif arguments.arguments.first.is_a?(Prism::DefNode)
116
+ visibility, _ = @visibility_stack.pop
117
+ @visibility_stack.push([name, visibility])
118
+ end
119
+
120
+ return
121
+ end
122
+
123
+ if @path&.include?(GEMFILE_NAME) && name == :gem && arguments
124
+ return unless @lenses_configuration.enabled?(:gemfileLinks)
125
+
126
+ first_argument = arguments.arguments.first
127
+ return unless first_argument.is_a?(Prism::StringNode)
128
+
129
+ remote = resolve_gem_remote(first_argument)
130
+ return unless remote
131
+
132
+ add_open_gem_remote_code_lens(node, remote)
133
+ end
134
+ end
135
+
136
+ sig { params(node: Prism::CallNode).void }
137
+ def on_call_node_leave(node)
138
+ _, prev_visibility = @visibility_stack.pop
139
+ @visibility_stack.push([prev_visibility, prev_visibility])
140
+ end
141
+
142
+ private
143
+
144
+ sig { params(node: Prism::Node, name: String, command: String, kind: Symbol).void }
145
+ def add_test_code_lens(node, name:, command:, kind:)
146
+ # don't add code lenses if the test library is not supported or unknown
147
+ return unless SUPPORTED_TEST_LIBRARIES.include?(DependencyDetector.instance.detected_test_library) && @path
148
+
149
+ arguments = [
150
+ @path,
151
+ name,
152
+ command,
153
+ {
154
+ start_line: node.location.start_line - 1,
155
+ start_column: node.location.start_column,
156
+ end_line: node.location.end_line - 1,
157
+ end_column: node.location.end_column,
158
+ },
159
+ ]
160
+
161
+ grouping_data = { group_id: @group_id_stack.last, kind: kind }
162
+ grouping_data[:id] = @group_id if kind == :group
163
+
164
+ @_response << create_code_lens(
165
+ node,
166
+ title: "Run",
167
+ command_name: "rubyLsp.runTest",
168
+ arguments: arguments,
169
+ data: { type: "test", **grouping_data },
170
+ )
171
+
172
+ @_response << create_code_lens(
173
+ node,
174
+ title: "Run In Terminal",
175
+ command_name: "rubyLsp.runTestInTerminal",
176
+ arguments: arguments,
177
+ data: { type: "test_in_terminal", **grouping_data },
178
+ )
179
+
180
+ @_response << create_code_lens(
181
+ node,
182
+ title: "Debug",
183
+ command_name: "rubyLsp.debugTest",
184
+ arguments: arguments,
185
+ data: { type: "debug", **grouping_data },
186
+ )
187
+ end
188
+
189
+ sig { params(gem_name: Prism::StringNode).returns(T.nilable(String)) }
190
+ def resolve_gem_remote(gem_name)
191
+ spec = Gem::Specification.stubs.find { |gem| gem.name == gem_name.content }&.to_spec
192
+ return if spec.nil?
193
+
194
+ [spec.homepage, spec.metadata["source_code_uri"]].compact.find do |page|
195
+ page.start_with?("https://github.com", "https://gitlab.com")
196
+ end
197
+ end
198
+
199
+ sig { params(class_name: String, method_name: T.nilable(String)).returns(String) }
200
+ def generate_test_command(class_name:, method_name: nil)
201
+ command = BASE_COMMAND + T.must(@path)
202
+
203
+ case DependencyDetector.instance.detected_test_library
204
+ when "minitest"
205
+ command += if method_name
206
+ " --name " + "/#{Shellwords.escape(class_name + "#" + method_name)}/"
207
+ else
208
+ " --name " + "/#{Shellwords.escape(class_name)}/"
209
+ end
210
+ when "test-unit"
211
+ command += " --testcase " + "/#{Shellwords.escape(class_name)}/"
212
+
213
+ if method_name
214
+ command += " --name " + Shellwords.escape(method_name)
215
+ end
216
+ end
217
+
218
+ command
219
+ end
220
+
221
+ sig { params(node: Prism::CallNode, remote: String).void }
222
+ def add_open_gem_remote_code_lens(node, remote)
223
+ @_response << create_code_lens(
224
+ node,
225
+ title: "Open remote",
226
+ command_name: "rubyLsp.openLink",
227
+ arguments: [remote],
228
+ data: { type: "link" },
229
+ )
230
+ end
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,275 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module RubyLsp
5
+ module Listeners
6
+ class Completion < Listener
7
+ extend T::Sig
8
+ extend T::Generic
9
+
10
+ ResponseType = type_member { { fixed: T::Array[Interface::CompletionItem] } }
11
+
12
+ sig { override.returns(ResponseType) }
13
+ attr_reader :_response
14
+
15
+ sig do
16
+ params(
17
+ index: RubyIndexer::Index,
18
+ nesting: T::Array[String],
19
+ typechecker_enabled: T::Boolean,
20
+ dispatcher: Prism::Dispatcher,
21
+ ).void
22
+ end
23
+ def initialize(index, nesting, typechecker_enabled, dispatcher)
24
+ super(dispatcher)
25
+ @_response = T.let([], ResponseType)
26
+ @index = index
27
+ @nesting = nesting
28
+ @typechecker_enabled = typechecker_enabled
29
+
30
+ dispatcher.register(
31
+ self,
32
+ :on_string_node_enter,
33
+ :on_constant_path_node_enter,
34
+ :on_constant_read_node_enter,
35
+ :on_call_node_enter,
36
+ )
37
+ end
38
+
39
+ sig { params(node: Prism::StringNode).void }
40
+ def on_string_node_enter(node)
41
+ @index.search_require_paths(node.content).map!(&:require_path).sort!.each do |path|
42
+ @_response << build_completion(T.must(path), node)
43
+ end
44
+ end
45
+
46
+ # Handle completion on regular constant references (e.g. `Bar`)
47
+ sig { params(node: Prism::ConstantReadNode).void }
48
+ def on_constant_read_node_enter(node)
49
+ return if DependencyDetector.instance.typechecker
50
+
51
+ name = node.slice
52
+ candidates = @index.prefix_search(name, @nesting)
53
+ candidates.each do |entries|
54
+ complete_name = T.must(entries.first).name
55
+ @_response << build_entry_completion(
56
+ complete_name,
57
+ name,
58
+ node,
59
+ entries,
60
+ top_level?(complete_name),
61
+ )
62
+ end
63
+ end
64
+
65
+ # Handle completion on namespaced constant references (e.g. `Foo::Bar`)
66
+ sig { params(node: Prism::ConstantPathNode).void }
67
+ def on_constant_path_node_enter(node)
68
+ return if DependencyDetector.instance.typechecker
69
+
70
+ name = node.slice
71
+
72
+ top_level_reference = if name.start_with?("::")
73
+ name = name.delete_prefix("::")
74
+ true
75
+ else
76
+ false
77
+ end
78
+
79
+ # If we're trying to provide completion for an aliased namespace, we need to first discover it's real name in
80
+ # order to find which possible constants match the desired search
81
+ *namespace, incomplete_name = name.split("::")
82
+ aliased_namespace = T.must(namespace).join("::")
83
+ namespace_entries = @index.resolve(aliased_namespace, @nesting)
84
+ return unless namespace_entries
85
+
86
+ real_namespace = @index.follow_aliased_namespace(T.must(namespace_entries.first).name)
87
+
88
+ candidates = @index.prefix_search("#{real_namespace}::#{incomplete_name}", top_level_reference ? [] : @nesting)
89
+ candidates.each do |entries|
90
+ # The only time we may have a private constant reference from outside of the namespace is if we're dealing
91
+ # with ConstantPath and the entry name doesn't start with the current nesting
92
+ first_entry = T.must(entries.first)
93
+ next if first_entry.visibility == :private && !first_entry.name.start_with?("#{@nesting}::")
94
+
95
+ constant_name = T.must(first_entry.name.split("::").last)
96
+
97
+ full_name = aliased_namespace.empty? ? constant_name : "#{aliased_namespace}::#{constant_name}"
98
+
99
+ @_response << build_entry_completion(
100
+ full_name,
101
+ name,
102
+ node,
103
+ entries,
104
+ top_level_reference || top_level?(T.must(entries.first).name),
105
+ )
106
+ end
107
+ end
108
+
109
+ sig { params(node: Prism::CallNode).void }
110
+ def on_call_node_enter(node)
111
+ return if @typechecker_enabled
112
+ return unless self_receiver?(node)
113
+
114
+ name = node.message
115
+ return unless name
116
+
117
+ receiver_entries = @index[@nesting.join("::")]
118
+ return unless receiver_entries
119
+
120
+ receiver = T.must(receiver_entries.first)
121
+
122
+ @index.prefix_search(name).each do |entries|
123
+ entry = entries.find { |e| e.is_a?(RubyIndexer::Entry::Member) && e.owner&.name == receiver.name }
124
+ next unless entry
125
+
126
+ @_response << build_method_completion(T.cast(entry, RubyIndexer::Entry::Member), node)
127
+ end
128
+ end
129
+
130
+ private
131
+
132
+ sig do
133
+ params(
134
+ entry: RubyIndexer::Entry::Member,
135
+ node: Prism::CallNode,
136
+ ).returns(Interface::CompletionItem)
137
+ end
138
+ def build_method_completion(entry, node)
139
+ name = entry.name
140
+
141
+ Interface::CompletionItem.new(
142
+ label: name,
143
+ filter_text: name,
144
+ text_edit: Interface::TextEdit.new(range: range_from_node(node), new_text: name),
145
+ kind: Constant::CompletionItemKind::METHOD,
146
+ label_details: Interface::CompletionItemLabelDetails.new(
147
+ detail: "(#{entry.parameters.map(&:decorated_name).join(", ")})",
148
+ description: entry.file_name,
149
+ ),
150
+ documentation: markdown_from_index_entries(name, entry),
151
+ )
152
+ end
153
+
154
+ sig { params(label: String, node: Prism::StringNode).returns(Interface::CompletionItem) }
155
+ def build_completion(label, node)
156
+ # We should use the content location as we only replace the content and not the delimiters of the string
157
+ loc = node.content_loc
158
+
159
+ Interface::CompletionItem.new(
160
+ label: label,
161
+ text_edit: Interface::TextEdit.new(
162
+ range: range_from_location(loc),
163
+ new_text: label,
164
+ ),
165
+ kind: Constant::CompletionItemKind::FILE,
166
+ )
167
+ end
168
+
169
+ sig do
170
+ params(
171
+ real_name: String,
172
+ incomplete_name: String,
173
+ node: Prism::Node,
174
+ entries: T::Array[RubyIndexer::Entry],
175
+ top_level: T::Boolean,
176
+ ).returns(Interface::CompletionItem)
177
+ end
178
+ def build_entry_completion(real_name, incomplete_name, node, entries, top_level)
179
+ first_entry = T.must(entries.first)
180
+ kind = case first_entry
181
+ when RubyIndexer::Entry::Class
182
+ Constant::CompletionItemKind::CLASS
183
+ when RubyIndexer::Entry::Module
184
+ Constant::CompletionItemKind::MODULE
185
+ when RubyIndexer::Entry::Constant
186
+ Constant::CompletionItemKind::CONSTANT
187
+ else
188
+ Constant::CompletionItemKind::REFERENCE
189
+ end
190
+
191
+ insertion_text = real_name.dup
192
+ filter_text = real_name.dup
193
+
194
+ # If we have two entries with the same name inside the current namespace and the user selects the top level
195
+ # option, we have to ensure it's prefixed with `::` or else we're completing the wrong constant. For example:
196
+ # If we have the index with ["Foo::Bar", "Bar"], and we're providing suggestions for `B` inside a `Foo` module,
197
+ # then selecting the `Foo::Bar` option needs to complete to `Bar` and selecting the top level `Bar` option needs
198
+ # to complete to `::Bar`.
199
+ if top_level
200
+ insertion_text.prepend("::")
201
+ filter_text.prepend("::")
202
+ end
203
+
204
+ # If the user is searching for a constant inside the current namespace, then we prefer completing the short name
205
+ # of that constant. E.g.:
206
+ #
207
+ # module Foo
208
+ # class Bar
209
+ # end
210
+ #
211
+ # Foo::B # --> completion inserts `Bar` instead of `Foo::Bar`
212
+ # end
213
+ @nesting.each do |namespace|
214
+ prefix = "#{namespace}::"
215
+ shortened_name = insertion_text.delete_prefix(prefix)
216
+
217
+ # If a different entry exists for the shortened name, then there's a conflict and we should not shorten it
218
+ conflict_name = "#{@nesting.join("::")}::#{shortened_name}"
219
+ break if real_name != conflict_name && @index[conflict_name]
220
+
221
+ insertion_text = shortened_name
222
+
223
+ # If the user is typing a fully qualified name `Foo::Bar::Baz`, then we should not use the short name (e.g.:
224
+ # `Baz`) as filtering. So we only shorten the filter text if the user is not including the namespaces in their
225
+ # typing
226
+ filter_text.delete_prefix!(prefix) unless incomplete_name.start_with?(prefix)
227
+ end
228
+
229
+ # When using a top level constant reference (e.g.: `::Bar`), the editor includes the `::` as part of the filter.
230
+ # For these top level references, we need to include the `::` as part of the filter text or else it won't match
231
+ # the right entries in the index
232
+ Interface::CompletionItem.new(
233
+ label: real_name,
234
+ filter_text: filter_text,
235
+ text_edit: Interface::TextEdit.new(
236
+ range: range_from_node(node),
237
+ new_text: insertion_text,
238
+ ),
239
+ kind: kind,
240
+ label_details: Interface::CompletionItemLabelDetails.new(
241
+ description: entries.map(&:file_name).join(","),
242
+ ),
243
+ documentation: markdown_from_index_entries(real_name, entries),
244
+ )
245
+ end
246
+
247
+ # Check if there are any conflicting names for `entry_name`, which would require us to use a top level reference.
248
+ # For example:
249
+ #
250
+ # ```ruby
251
+ # class Bar; end
252
+ #
253
+ # module Foo
254
+ # class Bar; end
255
+ #
256
+ # # in this case, the completion for `Bar` conflicts with `Foo::Bar`, so we can't suggest `Bar` as the
257
+ # # completion, but instead need to suggest `::Bar`
258
+ # B
259
+ # end
260
+ # ```
261
+ sig { params(entry_name: String).returns(T::Boolean) }
262
+ def top_level?(entry_name)
263
+ @nesting.length.downto(0).each do |i|
264
+ prefix = T.must(@nesting[0...i]).join("::")
265
+ full_name = prefix.empty? ? entry_name : "#{prefix}::#{entry_name}"
266
+ next if full_name == entry_name
267
+
268
+ return true if @index[full_name]
269
+ end
270
+
271
+ false
272
+ end
273
+ end
274
+ end
275
+ end