Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend the functionality of hover in rbs files. #397

Merged
merged 2 commits into from
Jul 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 74 additions & 1 deletion lib/steep/server/interaction_worker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,27 @@ def format_hover(content)
#{comment}

```rbs
#{content.decl.type}
#{retrieve_decl_information(content.decl)}
```
MD
when Services::HoverContent::InterfaceContent
comment = content.decl.comment&.string || ''

<<-MD
#{comment}

```rbs
#{retrieve_decl_information(content.decl)}
```
MD
when Services::HoverContent::ClassContent
comment = content.decl.comment&.string || ''

<<-MD
#{comment}

```rbs
#{retrieve_decl_information(content.decl)}
```
MD
when Services::HoverContent::VariableContent
Expand Down Expand Up @@ -188,6 +208,59 @@ def process_completion(job)
end
end

def name_and_params(name, params)
if params.empty?
"#{name}"
else
ps = params.each.map do |param|
s = ""
if param.skip_validation
s << "unchecked "
end
case param.variance
when :invariant
# nop
when :covariant
s << "out "
when :contravariant
s << "in "
end
s + param.name.to_s
end

"#{name}[#{ps.join(", ")}]"
end
end

def name_and_args(name, args)
if name && args
if args.empty?
"#{name}"
else
"#{name}[#{args.join(", ")}]"
end
end
end

def retrieve_decl_information(decl)
case decl
when RBS::AST::Declarations::Class
super_class = if super_class = decl.super_class
" < #{name_and_args(super_class.name, super_class.args)}"
end
"class #{name_and_params(decl.name, decl.type_params)}#{super_class}"
when RBS::AST::Declarations::Module
self_type = unless decl.self_types.empty?
" : #{decl.self_types.join(", ")}"
end
"module #{name_and_params(decl.name, decl.type_params)}#{self_type}"
when RBS::AST::Declarations::Alias
"type #{decl.name} = #{decl.type}"
when RBS::AST::Declarations::Interface
"interface #{name_and_params(decl.name, decl.type_params)}"
end
end

def format_completion_item(item)
range = LanguageServer::Protocol::Interface::Range.new(
start: LanguageServer::Protocol::Interface::Position.new(
Expand Down
22 changes: 22 additions & 0 deletions lib/steep/services/hover_content.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ class HoverContent
MethodCallContent = Struct.new(:node, :method_name, :type, :definition, :location, keyword_init: true)
DefinitionContent = Struct.new(:node, :method_name, :method_type, :definition, :location, keyword_init: true) do
TypeAliasContent = Struct.new(:location, :decl, keyword_init: true)
ClassContent = Struct.new(:location, :decl, keyword_init: true)
InterfaceContent = Struct.new(:location, :decl, keyword_init: true)

def comment_string
if comments = definition&.comments
comments.map {|c| c.string.chomp }.uniq.join("\n----\n")
Expand Down Expand Up @@ -76,6 +79,25 @@ def content_for(path:, line:, column:)
location: location,
decl: alias_decl
)
when RBS::Types::ClassInstance, RBS::Types::ClassSingleton
if hd == :name
env = service.latest_env
class_decl = env.class_decls[type.name]&.decls[0]&.decl or raise
location = tail[0].location[:name]
ClassContent.new(
location: location,
decl: class_decl
)
end
when RBS::Types::Interface
env = service.latest_env
interface_decl = env.interface_decls[type.name]&.decl or raise
location = type.location[:name]

InterfaceContent.new(
location: location,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use location[:name] here to highlight only name of the interface.

decl: interface_decl
)
end
end
end
Expand Down
84 changes: 82 additions & 2 deletions test/hover_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -218,9 +218,9 @@ def foo: (Integer x) -> String
end
end

def test_hover_on_rbs
def test_hover_alias_on_rbs
in_tmpdir do
service = typecheck_service()
service = typecheck_service

service.update(
changes: {
Expand All @@ -246,6 +246,86 @@ def f: (foo) -> void
end
end

def test_hover_class_singleton_on_rbs
in_tmpdir do
service = typecheck_service

service.update(
changes: {
Pathname("hello.rbs") => [ContentChange.string(<<RBS)]
class C
def foo: () -> singleton(String)
end
RBS
}
) {}

hover = HoverContent.new(service: service)
hover.content_for(path: Pathname("hello.rbs"), line: 2, column: 28).tap do |content|
assert_instance_of HoverContent::ClassContent, content
assert_instance_of RBS::Location, content.location
assert_equal content.location.start_line, 2
assert_equal content.location.start_column, 27
assert_instance_of RBS::AST::Declarations::Class, content.decl
end
end
end

def test_hover_class_instance_on_rbs
in_tmpdir do
service = typecheck_service

service.update(
changes: {
Pathname("hello.rbs") => [ContentChange.string(<<RBS)]
class Hoge end
class Qux
@foo: Hoge
end
RBS
}
) {}

hover = HoverContent.new(service: service)
hover.content_for(path: Pathname("hello.rbs"), line: 3, column: 9).tap do |content|
assert_instance_of HoverContent::ClassContent, content
assert_instance_of RBS::Location, content.location
assert_equal content.location.start_line, 3
assert_equal content.location.start_column, 8
assert_instance_of RBS::AST::Declarations::Class, content.decl
end
end
end

def test_hover_interface_on_rbs
in_tmpdir do
service = typecheck_service()

service.update(
changes: {
Pathname("hello.rbs") => [ContentChange.string(<<RBS)]
interface _Fooable
def foo: () -> nil
end

class Foo
def foo: (_Fooable) -> singleton(String)
end
RBS
}
) {}

hover = HoverContent.new(service: service)
hover.content_for(path: Pathname("hello.rbs"), line: 6, column: 13).tap do |content|
assert_instance_of HoverContent::InterfaceContent, content
assert_instance_of RBS::Location, content.location
assert_equal content.location.start_line, 6
assert_equal content.location.start_column, 12
assert_instance_of RBS::AST::Declarations::Interface, content.decl
end
end
end

def test_hover_on_syntax_error
in_tmpdir do
service = typecheck_service()
Expand Down
86 changes: 84 additions & 2 deletions test/interaction_worker_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ def test_handle_hover_job_success
end
end

def test_handle_hover_job_success_on_rbs
def test_handle_alias_hover_job_success_on_rbs
in_tmpdir do
project = Project.new(steepfile_path: current_dir + "Steepfile")
Project::DSL.parse(project, <<EOF)
Expand Down Expand Up @@ -205,14 +205,96 @@ def f: (foo) -> void


```rbs
::Integer | ::String
type ::foo = ::Integer | ::String
```
"
assert_equal({ kind: "markdown", value: expected_value }, response[:contents])
assert_equal({ start: { line: 4, character: 10 }, end: { line: 4, character: 13 }}, response[:range])
end
end

def test_handle_interface_hover_job_success_on_rbs
in_tmpdir do
project = Project.new(steepfile_path: current_dir + "Steepfile")
Project::DSL.parse(project, <<EOF)
target :lib do
check "lib"
signature "sig"
end
EOF

worker = InteractionWorker.new(project: project, reader: worker_reader, writer: worker_writer)

worker.service.update(
changes: {
Pathname("sig/hello.rbs") => [ContentChange.string(<<RBS)]
# here is your comments
interface _Fooable
def foo: () -> nil
end

class Test
def foo: (_Fooable) -> nil
end
RBS
}
) {}

response = worker.process_hover(InteractionWorker::HoverJob.new(path: Pathname("sig/hello.rbs"), line: 7, column: 13))

response = response.attributes
expected_value = "here is your comments


```rbs
interface ::_Fooable
```
"
assert_equal({ kind: "markdown", value: expected_value }, response[:contents])
assert_equal({ start: { line: 6, character: 12 }, end: { line: 6, character: 20 }}, response[:range])
end
end

def test_handle_class_hover_job_success_on_rbs
in_tmpdir do
project = Project.new(steepfile_path: current_dir + "Steepfile")
Project::DSL.parse(project, <<EOF)
target :lib do
check "lib"
signature "sig"
end
EOF

worker = InteractionWorker.new(project: project, reader: worker_reader, writer: worker_writer)

worker.service.update(
changes: {
Pathname("sig/hello.rbs") => [ContentChange.string(<<RBS)]
# here is your comments
class Foo [T] < Parent[T] end
class Parent [in T] end
module Hoge end
class Qux
@foo: Foo[Hoge]
end
RBS
}
) {}

response = worker.process_hover(InteractionWorker::HoverJob.new(path: Pathname("sig/hello.rbs"), line: 6, column: 10))

response = response.attributes
expected_value = "here is your comments


```rbs
class ::Foo[T] < ::Parent[T]
```
"
assert_equal({ kind: "markdown", value: expected_value }, response[:contents])
assert_equal({ start: { line: 5, character: 8 }, end: { line: 5, character: 11 }}, response[:range])
end
end

def test_handle_hover_invalid
in_tmpdir do
Expand Down