Skip to content

Commit

Permalink
Fix index usages in LSP features
Browse files Browse the repository at this point in the history
  • Loading branch information
vinistock committed Apr 17, 2024
1 parent 09417f6 commit 01f450c
Show file tree
Hide file tree
Showing 7 changed files with 85 additions and 100 deletions.
48 changes: 21 additions & 27 deletions lib/ruby_lsp/listeners/completion.rb
Expand Up @@ -42,13 +42,13 @@ def on_constant_read_node_enter(node)
return if name.nil?

candidates = @index.prefix_search_constants(name, @nesting)
candidates.each do |entries|
complete_name = T.must(entries.first).name
candidates.each do |entry|
complete_name = entry.name
@response_builder << build_entry_completion(
complete_name,
name,
node,
entries,
entry,
top_level?(complete_name),
)
end
Expand All @@ -73,31 +73,30 @@ def on_constant_path_node_enter(node)
# order to find which possible constants match the desired search
*namespace, incomplete_name = name.split("::")
aliased_namespace = T.must(namespace).join("::")
namespace_entries = @index.resolve_constant(aliased_namespace, @nesting)
return unless namespace_entries
namespace_entry = @index.resolve_constant(aliased_namespace, @nesting)
return unless namespace_entry

real_namespace = @index.follow_aliased_namespace(T.must(namespace_entries.first).name)
real_namespace = @index.follow_aliased_namespace(namespace_entry.name)

candidates = @index.prefix_search_constants(
"#{real_namespace}::#{incomplete_name}",
top_level_reference ? [] : @nesting,
)
candidates.each do |entries|
candidates.each do |entry|
# The only time we may have a private constant reference from outside of the namespace is if we're dealing
# with ConstantPath and the entry name doesn't start with the current nesting
first_entry = T.must(entries.first)
next if first_entry.visibility == :private && !first_entry.name.start_with?("#{@nesting}::")
next if entry.visibility == :private && !entry.name.start_with?("#{@nesting}::")

constant_name = T.must(first_entry.name.split("::").last)
constant_name = T.must(entry.name.split("::").last)

full_name = aliased_namespace.empty? ? constant_name : "#{aliased_namespace}::#{constant_name}"

@response_builder << build_entry_completion(
full_name,
name,
node,
entries,
top_level_reference || top_level?(T.must(entries.first).name),
entry,
top_level_reference || top_level?(entry.name),
)
end
end
Expand Down Expand Up @@ -166,16 +165,11 @@ def complete_require_relative(node)

sig { params(node: Prism::CallNode, name: String).void }
def complete_self_receiver_method(node, name)
receiver_entries = @index.get_constant(@nesting.join("::"))
return unless receiver_entries
receiver = @index.get_constant(@nesting.join("::"))
return unless receiver

receiver = T.must(receiver_entries.first)

@index.prefix_search_methods(name).each do |entries|
entry = entries.find { |e| e.is_a?(RubyIndexer::Entry::Member) && e.owner&.name == receiver.name }
next unless entry

@response_builder << build_method_completion(T.cast(entry, RubyIndexer::Entry::Member), node)
@index.prefix_search_methods(name).each do |entry|
@response_builder << build_method_completion(entry, node)
end
end

Expand All @@ -187,15 +181,16 @@ def complete_self_receiver_method(node, name)
end
def build_method_completion(entry, node)
name = entry.name
declarations = T.cast(entry.declarations, T::Array[RubyIndexer::Entry::MemberDeclaration])

Interface::CompletionItem.new(
label: name,
filter_text: name,
text_edit: Interface::TextEdit.new(range: range_from_location(T.must(node.message_loc)), new_text: name),
kind: Constant::CompletionItemKind::METHOD,
label_details: Interface::CompletionItemLabelDetails.new(
detail: "(#{entry.parameters.map(&:decorated_name).join(", ")})",
description: entry.file_name,
detail: "(#{T.must(declarations.first).parameters.map(&:decorated_name).join(", ")})",
description: declarations.map(&:file_name).join(","),
),
documentation: Interface::MarkupContent.new(
kind: "markdown",
Expand Down Expand Up @@ -224,13 +219,12 @@ def build_completion(label, node)
real_name: String,
incomplete_name: String,
node: Prism::Node,
entries: T::Array[RubyIndexer::Entry],
entry: RubyIndexer::Entry,
top_level: T::Boolean,
).returns(Interface::CompletionItem)
end
def build_entry_completion(real_name, incomplete_name, node, entries, top_level)
first_entry = T.must(entries.first)
kind = case first_entry
def build_entry_completion(real_name, incomplete_name, node, entry, top_level)
kind = case entry
when RubyIndexer::Entry::Class
Constant::CompletionItemKind::CLASS
when RubyIndexer::Entry::Module
Expand Down
23 changes: 11 additions & 12 deletions lib/ruby_lsp/listeners/definition.rb
Expand Up @@ -69,12 +69,12 @@ def handle_method_definition(node)
message = node.message
return unless message

methods = @index.resolve_method(message, @nesting.join("::"))
return unless methods
target_method = @index.resolve_method(message, @nesting.join("::"))
return unless target_method

methods.each do |target_method|
location = target_method.location
file_path = target_method.file_path
target_method.declarations.each do |declaration|
location = declaration.location
file_path = declaration.file_path
next if @typechecker_enabled && not_in_dependencies?(file_path)

@response_builder << Interface::Location.new(
Expand Down Expand Up @@ -131,20 +131,19 @@ def handle_require_definition(node)

sig { params(value: String).void }
def find_in_index(value)
entries = @index.resolve_constant(value, @nesting)
return unless entries
entry = @index.resolve_constant(value, @nesting)
return unless entry

# We should only allow jumping to the definition of private constants if the constant is defined in the same
# namespace as the reference
first_entry = T.must(entries.first)
return if first_entry.visibility == :private && first_entry.name != "#{@nesting.join("::")}::#{value}"
return if entry.visibility == :private && entry.name != "#{@nesting.join("::")}::#{value}"

entries.each do |entry|
location = entry.location
entry.declarations.each do |declaration|
location = declaration.location
# If the project has Sorbet, then we only want to handle go to definition for constants defined in gems, as an
# additional behavior on top of jumping to RBIs. Sorbet can already handle go to definition for all constants
# in the project, even if the files are typed false
file_path = entry.file_path
file_path = declaration.file_path
next if @typechecker_enabled && not_in_dependencies?(file_path)

@response_builder << Interface::Location.new(
Expand Down
15 changes: 7 additions & 8 deletions lib/ruby_lsp/listeners/hover.rb
Expand Up @@ -93,10 +93,10 @@ def on_call_node_enter(node)
message = node.message
return unless message

methods = @index.resolve_method(message, @nesting.join("::"))
return unless methods
target_method = @index.resolve_method(message, @nesting.join("::"))
return unless target_method

categorized_markdown_from_index_entries(message, methods).each do |category, content|
categorized_markdown_from_index_entries(message, target_method).each do |category, content|
@response_builder.push(content, category: category)
end
end
Expand All @@ -105,15 +105,14 @@ def on_call_node_enter(node)

sig { params(name: String, location: Prism::Location).void }
def generate_hover(name, location)
entries = @index.resolve_constant(name, @nesting)
return unless entries
entry = @index.resolve_constant(name, @nesting)
return unless entry

# We should only show hover for private constants if the constant is defined in the same namespace as the
# reference
first_entry = T.must(entries.first)
return if first_entry.visibility == :private && first_entry.name != "#{@nesting.join("::")}::#{name}"
return if entry.visibility == :private && entry.name != "#{@nesting.join("::")}::#{name}"

categorized_markdown_from_index_entries(name, entries).each do |category, content|
categorized_markdown_from_index_entries(name, entry).each do |category, content|
@response_builder.push(content, category: category)
end
end
Expand Down
11 changes: 5 additions & 6 deletions lib/ruby_lsp/listeners/signature_help.rb
Expand Up @@ -33,13 +33,12 @@ def on_call_node_enter(node)
message = node.message
return unless message

methods = @index.resolve_method(message, @nesting.join("::"))
return unless methods

target_method = methods.first
target_method = @index.resolve_method(message, @nesting.join("::"))
return unless target_method

parameters = target_method.parameters
declarations = T.cast(target_method.declarations, T::Array[RubyIndexer::Entry::MemberDeclaration])
# TODO: this is currently only showing the first declaration parameters, but a method can be overridden
parameters = T.must(declarations.first).parameters
name = target_method.name

# If the method doesn't have any parameters, there's no need to show signature help
Expand All @@ -65,7 +64,7 @@ def on_call_node_enter(node)
parameters: parameters.map { |param| Interface::ParameterInformation.new(label: param.name) },
documentation: Interface::MarkupContent.new(
kind: "markdown",
value: markdown_from_index_entries("", methods),
value: markdown_from_index_entries("", target_method),
),
),
],
Expand Down
13 changes: 6 additions & 7 deletions lib/ruby_lsp/requests/completion_resolve.rb
Expand Up @@ -39,22 +39,21 @@ def initialize(global_state, item)
sig { override.returns(Interface::CompletionItem) }
def perform
label = @item[:label]
entries = case @item[:kind]
entry = case @item[:kind]
when Constant::CompletionItemKind::CLASS, Constant::CompletionItemKind::MODULE,
Constant::CompletionItemKind::CONSTANT
@index.get_constant(label) || []
T.must(@index.get_constant(label))
else
@index.get_method(label) || []
T.must(@index.get_method(label))
end
file_names = entry.declarations.take(MAX_DOCUMENTATION_ENTRIES).map(&:file_name)

Interface::CompletionItem.new(
label: label,
label_details: Interface::CompletionItemLabelDetails.new(
description: entries.take(MAX_DOCUMENTATION_ENTRIES).map(&:file_name).join(","),
),
label_details: Interface::CompletionItemLabelDetails.new(description: file_names.join(",")),
documentation: Interface::MarkupContent.new(
kind: "markdown",
value: markdown_from_index_entries(label, entries, MAX_DOCUMENTATION_ENTRIES),
value: markdown_from_index_entries(label, entry, MAX_DOCUMENTATION_ENTRIES),
),
)
end
Expand Down
25 changes: 9 additions & 16 deletions lib/ruby_lsp/requests/support/common.rb
Expand Up @@ -85,17 +85,16 @@ def self_receiver?(node)
sig do
params(
title: String,
entries: T.any(T::Array[RubyIndexer::Entry], RubyIndexer::Entry),
entry: RubyIndexer::Entry,
max_entries: T.nilable(Integer),
).returns(T::Hash[Symbol, String])
end
def categorized_markdown_from_index_entries(title, entries, max_entries = nil)
def categorized_markdown_from_index_entries(title, entry, max_entries = nil)
markdown_title = "```ruby\n#{title}\n```"
definitions = []
content = +""
entries = Array(entries)
entries_to_format = max_entries ? entries.take(max_entries) : entries
entries_to_format.each do |entry|
declarations = max_entries ? entry.declarations.take(max_entries) : entry.declarations
declarations.each do |entry|
loc = entry.location

# We always handle locations as zero based. However, for file links in Markdown we need them to be one
Expand All @@ -111,8 +110,8 @@ def categorized_markdown_from_index_entries(title, entries, max_entries = nil)
content << "\n\n#{entry.comments.join("\n")}" unless entry.comments.empty?
end

additional_entries_text = if max_entries && entries.length > max_entries
additional = entries.length - max_entries
additional_entries_text = if max_entries && declarations.length > max_entries
additional = declarations.length - max_entries
" | #{additional} other#{additional > 1 ? "s" : ""}"
else
""
Expand All @@ -125,15 +124,9 @@ def categorized_markdown_from_index_entries(title, entries, max_entries = nil)
}
end

sig do
params(
title: String,
entries: T.any(T::Array[RubyIndexer::Entry], RubyIndexer::Entry),
max_entries: T.nilable(Integer),
).returns(String)
end
def markdown_from_index_entries(title, entries, max_entries = nil)
categorized_markdown = categorized_markdown_from_index_entries(title, entries, max_entries)
sig { params(title: String, entry: RubyIndexer::Entry, max_entries: T.nilable(Integer)).returns(String) }
def markdown_from_index_entries(title, entry, max_entries = nil)
categorized_markdown = categorized_markdown_from_index_entries(title, entry, max_entries)

<<~MARKDOWN.chomp
#{categorized_markdown[:title]}
Expand Down
50 changes: 26 additions & 24 deletions lib/ruby_lsp/requests/workspace_symbol.rb
Expand Up @@ -32,35 +32,37 @@ def initialize(global_state, query)

sig { override.returns(T::Array[Interface::WorkspaceSymbol]) }
def perform
@index.fuzzy_search(@query).filter_map do |entry|
# If the project is using Sorbet, we let Sorbet handle symbols defined inside the project itself and RBIs, but
# we still return entries defined in gems to allow developers to jump directly to the source
file_path = entry.file_path
next if @global_state.typechecker && not_in_dependencies?(file_path)
@index.fuzzy_search(@query).flat_map do |entry|
entry.declarations.filter_map do |declaration|
# If the project is using Sorbet, we let Sorbet handle symbols defined inside the project itself and RBIs,
# but we still return entries defined in gems to allow developers to jump directly to the source
file_path = declaration.file_path
next if @global_state.typechecker && not_in_dependencies?(file_path)

# We should never show private symbols when searching the entire workspace
next if entry.visibility == :private
# We should never show private symbols when searching the entire workspace
next if entry.visibility == :private

kind = kind_for_entry(entry)
loc = entry.location
kind = kind_for_entry(entry)
loc = declaration.location

# We use the namespace as the container name, but we also use the full name as the regular name. The reason we
# do this is to allow people to search for fully qualified names (e.g.: `Foo::Bar`). If we only included the
# short name `Bar`, then searching for `Foo::Bar` would not return any results
*container, _short_name = entry.name.split("::")
# We use the namespace as the container name, but we also use the full name as the regular name. The reason
# we do this is to allow people to search for fully qualified names (e.g.: `Foo::Bar`). If we only included
# the short name `Bar`, then searching for `Foo::Bar` would not return any results
*container, _short_name = entry.name.split("::")

Interface::WorkspaceSymbol.new(
name: entry.name,
container_name: T.must(container).join("::"),
kind: kind,
location: Interface::Location.new(
uri: URI::Generic.from_path(path: file_path).to_s,
range: Interface::Range.new(
start: Interface::Position.new(line: loc.start_line - 1, character: loc.start_column),
end: Interface::Position.new(line: loc.end_line - 1, character: loc.end_column),
Interface::WorkspaceSymbol.new(
name: entry.name,
container_name: T.must(container).join("::"),
kind: kind,
location: Interface::Location.new(
uri: URI::Generic.from_path(path: file_path).to_s,
range: Interface::Range.new(
start: Interface::Position.new(line: loc.start_line - 1, character: loc.start_column),
end: Interface::Position.new(line: loc.end_line - 1, character: loc.end_column),
),
),
),
)
)
end
end
end

Expand Down

0 comments on commit 01f450c

Please sign in to comment.