Skip to content

Commit

Permalink
Merge pull request #2103 from Shopify/atvs-types-hierarchy
Browse files Browse the repository at this point in the history
Add support for type hierarchy requests
  • Loading branch information
Morriar committed Jun 18, 2024
2 parents d2d24b3 + 188fed0 commit a8a03f0
Show file tree
Hide file tree
Showing 12 changed files with 406 additions and 22 deletions.
2 changes: 2 additions & 0 deletions lib/ruby_lsp/requests.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ module Requests
autoload :ShowSyntaxTree, "ruby_lsp/requests/show_syntax_tree"
autoload :WorkspaceSymbol, "ruby_lsp/requests/workspace_symbol"
autoload :SignatureHelp, "ruby_lsp/requests/signature_help"
autoload :PrepareTypeHierarchy, "ruby_lsp/requests/prepare_type_hierarchy"
autoload :TypeHierarchySupertypes, "ruby_lsp/requests/type_hierarchy_supertypes"

# :nodoc:
module Support
Expand Down
88 changes: 88 additions & 0 deletions lib/ruby_lsp/requests/prepare_type_hierarchy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# typed: strict
# frozen_string_literal: true

module RubyLsp
module Requests
# ![Prepare type hierarchy demo](../../prepare_type_hierarchy.gif)
#
# The [prepare type hierarchy
# request](https://microsoft.github.io/language-server-protocol/specification#textDocument_prepareTypeHierarchy)
# displays the list of ancestors (supertypes) and descendants (subtypes) for the selected type.
#
# Currently only supports supertypes due to a limitation of the index.
#
# # Example
#
# ```ruby
# class Foo; end
# class Bar < Foo; end
#
# puts Bar # <-- right click on `Bar` and select "Show Type Hierarchy"
# ```
class PrepareTypeHierarchy < Request
extend T::Sig

include Support::Common

class << self
extend T::Sig

sig { returns(Interface::TypeHierarchyOptions) }
def provider
Interface::TypeHierarchyOptions.new
end
end

sig do
params(
document: Document,
index: RubyIndexer::Index,
position: T::Hash[Symbol, T.untyped],
).void
end
def initialize(document, index, position)
super()

@document = document
@index = index
@position = position
end

sig { override.returns(T.nilable(T::Array[Interface::TypeHierarchyItem])) }
def perform
context = @document.locate_node(
@position,
node_types: [
Prism::ConstantReadNode,
Prism::ConstantWriteNode,
Prism::ConstantPathNode,
],
)

node = context.node
parent = context.parent
return unless node && parent

target = determine_target(node, parent, @position)
entries = @index.resolve(target.slice, context.nesting)
return unless entries

# While the spec allows for multiple entries, VSCode seems to only support one
# We'll just return the first one for now
first_entry = T.must(entries.first)

range = range_from_location(first_entry.location)

[
Interface::TypeHierarchyItem.new(
name: first_entry.name,
kind: kind_for_entry(first_entry),
uri: URI::Generic.from_path(path: first_entry.file_path).to_s,
range: range,
selection_range: range,
),
]
end
end
end
end
20 changes: 19 additions & 1 deletion lib/ruby_lsp/requests/support/common.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def range_from_node(node)
)
end

sig { params(location: Prism::Location).returns(Interface::Range) }
sig { params(location: T.any(Prism::Location, RubyIndexer::Location)).returns(Interface::Range) }
def range_from_location(location)
Interface::Range.new(
start: Interface::Position.new(
Expand Down Expand Up @@ -186,6 +186,24 @@ def each_constant_path_part(node, &block)
current = current.parent
end
end

sig { params(entry: RubyIndexer::Entry).returns(T.nilable(Integer)) }
def kind_for_entry(entry)
case entry
when RubyIndexer::Entry::Class
Constant::SymbolKind::CLASS
when RubyIndexer::Entry::Module
Constant::SymbolKind::NAMESPACE
when RubyIndexer::Entry::Constant
Constant::SymbolKind::CONSTANT
when RubyIndexer::Entry::Method
entry.name == "initialize" ? Constant::SymbolKind::CONSTRUCTOR : Constant::SymbolKind::METHOD
when RubyIndexer::Entry::Accessor
Constant::SymbolKind::PROPERTY
when RubyIndexer::Entry::InstanceVariable
Constant::SymbolKind::FIELD
end
end
end
end
end
Expand Down
91 changes: 91 additions & 0 deletions lib/ruby_lsp/requests/type_hierarchy_supertypes.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# typed: strict
# frozen_string_literal: true

module RubyLsp
module Requests
# ![Type hierarchy supertypes demo](../../type_hierarchy_supertypes.gif)
#
# The [type hierarchy supertypes
# request](https://microsoft.github.io/language-server-protocol/specification#typeHierarchy_supertypes)
# displays the list of ancestors (supertypes) for the selected type.
#
# # Example
#
# ```ruby
# class Foo; end
# class Bar < Foo; end
#
# puts Bar # <-- right click on `Bar` and select "Show Type Hierarchy"
# ```
class TypeHierarchySupertypes < Request
extend T::Sig

include Support::Common

sig { params(index: RubyIndexer::Index, item: T::Hash[Symbol, T.untyped]).void }
def initialize(index, item)
super()

@index = index
@item = item
end

sig { override.returns(T.nilable(T::Array[Interface::TypeHierarchyItem])) }
def perform
name = @item[:name]
entries = @index[name]

parents = T.let(Set.new, T::Set[RubyIndexer::Entry::Namespace])
return unless entries&.any?

entries.each do |entry|
next unless entry.is_a?(RubyIndexer::Entry::Namespace)

if entry.is_a?(RubyIndexer::Entry::Class)
parent_class_name = entry.parent_class
if parent_class_name
resolved_parent_entries = @index.resolve(parent_class_name, entry.nesting)
resolved_parent_entries&.each do |entry|
next unless entry.is_a?(RubyIndexer::Entry::Class)

parents << entry
end
end
end

entry.mixin_operations.each do |mixin_operation|
next if mixin_operation.is_a?(RubyIndexer::Entry::Extend)

mixin_name = mixin_operation.module_name
resolved_mixin_entries = @index.resolve(mixin_name, entry.nesting)
next unless resolved_mixin_entries

resolved_mixin_entries.each do |mixin_entry|
next unless mixin_entry.is_a?(RubyIndexer::Entry::Module)

parents << mixin_entry
end
end
end

parents.map { |entry| hierarchy_item(entry) }
end

private

sig { params(entry: RubyIndexer::Entry).returns(Interface::TypeHierarchyItem) }
def hierarchy_item(entry)
range = range_from_location(entry.location)

Interface::TypeHierarchyItem.new(
name: entry.name,
kind: kind_for_entry(entry),
uri: URI::Generic.from_path(path: entry.file_path).to_s,
range: range,
selection_range: range,
detail: entry.file_name,
)
end
end
end
end
20 changes: 0 additions & 20 deletions lib/ruby_lsp/requests/workspace_symbol.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,26 +64,6 @@ def perform
)
end
end

private

sig { params(entry: RubyIndexer::Entry).returns(T.nilable(Integer)) }
def kind_for_entry(entry)
case entry
when RubyIndexer::Entry::Class
Constant::SymbolKind::CLASS
when RubyIndexer::Entry::Module
Constant::SymbolKind::NAMESPACE
when RubyIndexer::Entry::Constant
Constant::SymbolKind::CONSTANT
when RubyIndexer::Entry::Method
entry.name == "initialize" ? Constant::SymbolKind::CONSTRUCTOR : Constant::SymbolKind::METHOD
when RubyIndexer::Entry::Accessor
Constant::SymbolKind::PROPERTY
when RubyIndexer::Entry::InstanceVariable
Constant::SymbolKind::FIELD
end
end
end
end
end
35 changes: 35 additions & 0 deletions lib/ruby_lsp/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ def process_message(message)
text_document_signature_help(message)
when "textDocument/definition"
text_document_definition(message)
when "textDocument/prepareTypeHierarchy"
text_document_prepare_type_hierarchy(message)
when "typeHierarchy/supertypes"
type_hierarchy_supertypes(message)
when "typeHierarchy/subtypes"
type_hierarchy_subtypes(message)
when "workspace/didChangeWatchedFiles"
workspace_did_change_watched_files(message)
when "workspace/symbol"
Expand Down Expand Up @@ -162,6 +168,7 @@ def run_initialize(message)
inlay_hint_provider = Requests::InlayHints.provider if enabled_features["inlayHint"]
completion_provider = Requests::Completion.provider if enabled_features["completion"]
signature_help_provider = Requests::SignatureHelp.provider if enabled_features["signatureHelp"]
type_hierarchy_provider = Requests::PrepareTypeHierarchy.provider if enabled_features["typeHierarchy"]

response = {
capabilities: Interface::ServerCapabilities.new(
Expand All @@ -187,6 +194,7 @@ def run_initialize(message)
definition_provider: enabled_features["definition"],
workspace_symbol_provider: enabled_features["workspaceSymbol"] && !@global_state.has_type_checker,
signature_help_provider: signature_help_provider,
type_hierarchy_provider: type_hierarchy_provider,
experimental: {
addon_detection: true,
},
Expand Down Expand Up @@ -673,6 +681,33 @@ def text_document_show_syntax_tree(message)
send_message(Result.new(id: message[:id], response: response))
end

sig { params(message: T::Hash[Symbol, T.untyped]).void }
def text_document_prepare_type_hierarchy(message)
params = message[:params]
response = Requests::PrepareTypeHierarchy.new(
@store.get(params.dig(:textDocument, :uri)),
@global_state.index,
params[:position],
).perform
send_message(Result.new(id: message[:id], response: response))
end

sig { params(message: T::Hash[Symbol, T.untyped]).void }
def type_hierarchy_supertypes(message)
response = Requests::TypeHierarchySupertypes.new(
@global_state.index,
message.dig(:params, :item),
).perform
send_message(Result.new(id: message[:id], response: response))
end

sig { params(message: T::Hash[Symbol, T.untyped]).void }
def type_hierarchy_subtypes(message)
# TODO: implement subtypes
# The current index representation doesn't allow us to find the children of an entry.
send_message(Result.new(id: message[:id], response: nil))
end

sig { params(message: T::Hash[Symbol, T.untyped]).void }
def workspace_dependencies(message)
response = begin
Expand Down
Binary file added misc/prepare_type_hierarchy.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added misc/type_hierarchy_supertypes.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit a8a03f0

Please sign in to comment.