module Steep
module Server
module LSPFormatter
include Services
LSP = LanguageServer::Protocol
module_function
def markup_content(string = nil, &block)
if block
string = yield()
end
if string
LSP::Interface::MarkupContent.new(kind: LSP::Constant::MarkupKind::MARKDOWN, value: string)
end
end
def format_hover_content(content)
case content
when HoverProvider::Ruby::VariableContent
local_variable(content.name, content.type)
when HoverProvider::Ruby::MethodCallContent
io = StringIO.new
call = content.method_call
case call
when TypeInference::MethodCall::Typed
io.puts <<~MD
```rbs
#{call.actual_method_type.type.return_type}
```
----
MD
method_types = call.method_decls.map(&:method_type)
if call.is_a?(TypeInference::MethodCall::Special)
method_types = [
call.actual_method_type.with(
type: call.actual_method_type.type.with(return_type: call.return_type)
)
]
header = <<~MD
**💡 Custom typing rule applies**
----
MD
end
when TypeInference::MethodCall::Error
method_types = call.method_decls.map {|decl| decl.method_type }
header = <<~MD
**🚨 No compatible method type found**
----
MD
end
method_names = call.method_decls.map {|decl| decl.method_name.relative }
docs = call.method_decls.map {|decl| [decl.method_name, decl.method_def.comment] }.to_h
if header
io.puts header
end
io.puts(
format_method_item_doc(method_types, method_names, docs)
)
io.string
when HoverProvider::Ruby::DefinitionContent
io = StringIO.new
method_name =
if content.method_name.is_a?(SingletonMethodName)
"self.#{content.method_name.method_name}"
else
content.method_name.method_name
end
prefix_size = "def ".size + method_name.size
method_types = content.definition.method_types
io.puts <<~MD
```rbs
def #{method_name}: #{method_types.join("\n" + " "*prefix_size + "| ") }
```
----
MD
if content.definition.method_types.size > 1
io.puts "**Internal method type**"
io.puts <<~MD
```rbs
#{content.method_type}
```
----
MD
end
io.puts format_comments(
content.definition.comments.map {|comment|
[content.method_name.relative.to_s, comment] #: [String, RBS::AST::Comment?]
}
)
io.string
when HoverProvider::Ruby::ConstantContent
io = StringIO.new
decl_summary =
case
when decl = content.class_decl
declaration_summary(decl.primary.decl)
when decl = content.constant_decl
declaration_summary(decl.decl)
when decl = content.class_alias
declaration_summary(decl.decl)
end
io.puts <<~MD
```rbs
#{decl_summary}
```
MD
comments = content.comments.map {|comment|
[content.full_name.relative!.to_s, comment] #: [String, RBS::AST::Comment?]
}
unless comments.all?(&:nil?)
io.puts "----"
io.puts format_comments(comments)
end
io.string
when HoverProvider::Ruby::TypeContent
<<~MD
```rbs
#{content.type}
```
MD
when HoverProvider::Ruby::TypeAssertionContent
<<~MD
```rbs
#{content.asserted_type}
```
↑ Converted from `#{content.original_type.to_s}`
MD
when HoverProvider::RBS::TypeAliasContent, HoverProvider::RBS::InterfaceContent
io = StringIO.new()
io.puts <<~MD
```rbs
#{declaration_summary(content.decl)}
```
MD
if comment = content.decl.comment
io.puts
io.puts "----"
io.puts format_comment(comment, header: content.decl.name.relative!.to_s)
end
io.string
when HoverProvider::RBS::ClassContent
io = StringIO.new
io << <<~MD
```rbs
#{declaration_summary(content.decl)}
```
MD
if content.decl.comment
io.puts "----"
class_name =
case content.decl
when RBS::AST::Declarations::ModuleAlias, RBS::AST::Declarations::ClassAlias
content.decl.new_name
when RBS::AST::Declarations::Class, RBS::AST::Declarations::Module
content.decl.name
else
raise
end
io << format_comments([[class_name.relative!.to_s, content.decl.comment]])
end
io.string
else
raise content.class.to_s
end
end
def format_completion_docs(item)
case item
when Services::CompletionProvider::LocalVariableItem
local_variable(item.identifier, item.type)
when Services::CompletionProvider::ConstantItem
io = StringIO.new
io.puts <<~MD
```rbs
#{declaration_summary(item.decl)}
```
MD
unless item.comments.all?(&:nil?)
io.puts "----"
io.puts format_comments(
item.comments.map {|comment|
[item.full_name.relative!.to_s, comment] #: [String, RBS::AST::Comment?]
}
)
end
io.string
when Services::CompletionProvider::InstanceVariableItem
instance_variable(item.identifier, item.type)
when Services::CompletionProvider::SimpleMethodNameItem
format_method_item_doc(item.method_types, [], { item.method_name => item.method_member.comment })
when Services::CompletionProvider::ComplexMethodNameItem
method_names = item.method_names.map(&:relative).uniq
comments = item.method_definitions.transform_values {|member| member.comment }
format_method_item_doc(item.method_types, method_names, comments)
when Services::CompletionProvider::GeneratedMethodNameItem
format_method_item_doc(item.method_types, [], {}, "🤖 Generated method for receiver type")
when Services::CompletionProvider::TypeNameItem
io = StringIO.new
io.puts <<~MD
```rbs
#{declaration_summary(item.decl)}
```
MD
unless item.comments.empty?
io.puts "----"
io.puts format_comments(
item.comments.map {|comment|
[item.absolute_type_name.relative!.to_s, comment] #: [String, RBS::AST::Comment?]
}
)
end
io.string
when Services::CompletionProvider::KeywordArgumentItem
<<~MD
**Keyword argument**: `#{item.identifier}`
MD
end
end
def format_rbs_completion_docs(type_name, decl, comments)
io = StringIO.new
io.puts <<~MD
```rbs
#{declaration_summary(decl)}
```
MD
unless comments.empty?
io.puts
io.puts "----"
io.puts format_comments(
comments.map {|comment|
[type_name.relative!.to_s, comment] #: [String, RBS::AST::Comment?]
}
)
end
io.string
end
def format_comments(comments)
io = StringIO.new
with_docs = [] #: Array[[String, RBS::AST::Comment]]
without_docs = [] #: Array[String]
comments.each do |title, comment|
if comment
with_docs << [title, comment]
else
without_docs << title
end
end
unless with_docs.empty?
with_docs.each do |title, comment|
io.puts format_comment(comment, header: title)
io.puts
end
unless without_docs.empty?
io.puts
io.puts "----"
if without_docs.size == 1
io.puts "🔍 One more definition without docs"
else
io.puts "🔍 #{without_docs.size} more definitions without docs"
end
end
end
io.string
end
def format_comment(comment, header: nil, &block)
return unless comment
io = StringIO.new
if header
io.puts "### 📚 #{header.gsub("_", "\\_")}"
io.puts
end
io.puts comment.string.rstrip.gsub(/^[ \t]*<!--(?~-->)-->\n/, "").gsub(/\A([ \t]*\n)+/, "")
if block
yield io.string
else
io.string
end
end
def local_variable(name, type)
<<~MD
**Local variable** `#{name}: #{type}`
MD
end
def instance_variable(name, type)
<<~MD
**Instance variable** `#{name}: #{type}`
MD
end
def name_and_params(name, params)
if params.empty?
"#{name}"
else
ps = params.each.map do |param|
s = ""
if param.unchecked?
s << "unchecked "
end
case param.variance
when :invariant
# nop
when :covariant
s << "out "
when :contravariant
s << "in "
end
s << param.name.to_s
if param.upper_bound
s << " < #{param.upper_bound.to_s}"
end
s
end
"#{name}[#{ps.join(", ")}]"
end
end
def name_and_args(name, args)
if args.empty?
"#{name}"
else
"#{name}[#{args.map(&:to_s).join(", ")}]"
end
end
def declaration_summary(decl)
# Note that all names in the declarations is absolute
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.relative!, decl.type_params)}#{super_class}"
when RBS::AST::Declarations::Module
self_type = unless decl.self_types.empty?
" : #{decl.self_types.map {|s| name_and_args(s.name, s.args) }.join(", ")}"
end
"module #{name_and_params(decl.name.relative!, decl.type_params)}#{self_type}"
when RBS::AST::Declarations::TypeAlias
"type #{name_and_params(decl.name.relative!, decl.type_params)} = #{decl.type}"
when RBS::AST::Declarations::Interface
"interface #{name_and_params(decl.name.relative!, decl.type_params)}"
when RBS::AST::Declarations::ClassAlias
"class #{decl.new_name.relative!} = #{decl.old_name}"
when RBS::AST::Declarations::ModuleAlias
"module #{decl.new_name.relative!} = #{decl.old_name}"
when RBS::AST::Declarations::Global
"#{decl.name}: #{decl.type}"
when RBS::AST::Declarations::Constant
"#{decl.name.relative!}: #{decl.type}"
end
end
def format_method_item_doc(method_types, method_names, comments, footer = "")
io = StringIO.new
io.puts "**Method type**:"
io.puts "```rbs"
if method_types.size == 1
io.puts method_types[0].to_s
else
io.puts " #{method_types.join("\n| ")}"
end
io.puts "```"
if method_names.size > 1
io.puts "**Possible methods**: #{method_names.map {|type| "`#{type.to_s}`" }.join(", ")}"
io.puts
end
unless comments.each_value.all?(&:nil?)
io.puts "----"
io.puts format_comments(comments.transform_keys {|name| name.relative.to_s }.entries)
end
unless footer.empty?
io.puts footer.rstrip
end
io.string
end
end
end
end