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

Improve hover #117

Merged
merged 11 commits into from
Feb 16, 2020
2 changes: 2 additions & 0 deletions lib/steep.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
require "steep/typing"
require "steep/errors"
require "steep/type_construction"
require "steep/type_inference/context"
require "steep/type_inference/send_args"
require "steep/type_inference/block_params"
require "steep/type_inference/constant_env"
Expand All @@ -72,6 +73,7 @@
require "steep/project/target"
require "steep/project/dsl"
require "steep/project/file_loader"
require "steep/project/hover_content"
require "steep/drivers/utils/driver_helper"
require "steep/drivers/check"
require "steep/drivers/validate"
Expand Down
71 changes: 61 additions & 10 deletions lib/steep/drivers/langserver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ def run_type_check()
[diagnostics_raw(source.status.error.message, source.status.location)]
when Project::SourceFile::ParseErrorStatus
[]
when Project::SourceFile::TypeCheckErrorStatus
[]
end

report_diagnostics source.path, diagnostics
Expand Down Expand Up @@ -264,22 +266,71 @@ def diagnostic_for_type_error(error)
def response_to_hover(path:, line:, column:)
Steep.logger.info { "path=#{path}, line=#{line}, column=#{column}" }

# line in LSP is zero-origin
project.type_of_node(path: path, line: line + 1, column: column) do |type, node|
Steep.logger.warn { "node = #{node.type}, type = #{type.to_s}" }

start_position = { line: node.location.line - 1, character: node.location.column }
end_position = { line: node.location.last_line - 1, character: node.location.last_column }
range = { start: start_position, end: end_position }

Steep.logger.warn { "range = #{range.inspect}" }
hover = Project::HoverContent.new(project: project)
content = hover.content_for(path: path, line: line+1, column: column+1)
if content
range = content.location.yield_self do |location|
start_position = { line: location.line - 1, character: location.column }
end_position = { line: location.last_line - 1, character: location.last_column }
{ start: start_position, end: end_position }
end

LanguageServer::Protocol::Interface::Hover.new(
contents: { kind: "markdown", value: "`#{type}`" },
contents: { kind: "markdown", value: format_hover(content) },
range: range
)
end
end

def format_hover(content)
case content
when Project::HoverContent::VariableContent
"`#{content.name}`: `#{content.type.to_s}`"
when Project::HoverContent::MethodCallContent
method_name = case content.method_name
when Project::HoverContent::InstanceMethodName
"#{content.method_name.class_name}##{content.method_name.method_name}"
when Project::HoverContent::SingletonMethodName
"#{content.method_name.class_name}.#{content.method_name.method_name}"
else
nil
end

if method_name
string = <<HOVER
```
#{method_name} ~> #{content.type}
```
HOVER
if content.definition
if content.definition.comment
string << "\n----\n\n#{content.definition.comment.string}"
end

string << "\n----\n\n#{content.definition.method_types.map {|x| "- `#{x}`\n" }.join()}"
end
else
"`#{content.type}`"
end
when Project::HoverContent::DefinitionContent
string = <<HOVER
```
def #{content.method_name}: #{content.method_type}
```
HOVER
if (comment = content.definition.comment)
string << "\n----\n\n#{comment.string}\n"
end

if content.definition.method_types.size > 1
string << "\n----\n\n#{content.definition.method_types.map {|x| "- `#{x}`\n" }.join()}"
end

string
when Project::HoverContent::TypeContent
"`#{content.type}`"
end
end
end
end
end
14 changes: 13 additions & 1 deletion lib/steep/interface/substitution.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
module Steep
module Interface
class Substitution
class InvalidSubstitutionError < StandardError
attr_reader :vars_size
attr_reader :types_size

def initialize(vars_size:, types_size:)
@var_size = vars_size
@types_size = types_size

super "Invalid substitution: vars.size=#{vars_size}, types.size=#{types_size}"
end
end

attr_reader :dictionary
attr_reader :instance_type
attr_reader :module_type
Expand Down Expand Up @@ -42,7 +54,7 @@ def key?(var)
def self.build(vars, types = nil, instance_type: AST::Types::Instance.new, module_type: AST::Types::Class.new, self_type: AST::Types::Self.new)
types ||= vars.map {|var| AST::Types::Var.fresh(var) }

raise "Invalid substitution: vars.size=#{vars.size}, types.size=#{types.size}" unless vars.size == types.size
raise InvalidSubstitutionError.new(vars_size: vars.size, types_size: types.size) unless vars.size == types.size

dic = vars.zip(types).each.with_object({}) do |(var, type), d|
d[var] = type
Expand Down
1 change: 1 addition & 0 deletions lib/steep/project.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def type_of_node(path:, line:, column:)
source_file = targets.map {|target| target.source_files[path] }.compact[0]

if source_file

case (status = source_file.status)
when SourceFile::TypeCheckStatus
node = status.source.find_node(line: line, column: column)
Expand Down
31 changes: 18 additions & 13 deletions lib/steep/project/file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class SourceFile
ParseErrorStatus = Struct.new(:error, keyword_init: true)
AnnotationSyntaxErrorStatus = Struct.new(:error, :location, keyword_init: true)
TypeCheckStatus = Struct.new(:typing, :source, :timestamp, keyword_init: true)
TypeCheckErrorStatus = Struct.new(:error, keyword_init: true)

def initialize(path:)
@path = path
Expand Down Expand Up @@ -62,20 +63,22 @@ def type_check(subtyping, env_updated_at)
checker: subtyping,
annotations: annotations,
source: source,
self_type: AST::Builtin::Object.instance_type,
block_context: nil,
module_context: TypeConstruction::ModuleContext.new(
instance_type: nil,
module_type: nil,
implement_name: nil,
current_namespace: AST::Namespace.root,
const_env: const_env,
class_name: nil
context: TypeInference::Context.new(
block_context: nil,
module_context: TypeInference::Context::ModuleContext.new(
instance_type: nil,
module_type: nil,
implement_name: nil,
current_namespace: AST::Namespace.root,
const_env: const_env,
class_name: nil
),
method_context: nil,
break_context: nil,
self_type: AST::Builtin::Object.instance_type,
type_env: type_env
),
method_context: nil,
typing: typing,
break_context: nil,
type_env: type_env
typing: typing
)

construction.synthesize(source.node)
Expand All @@ -86,6 +89,8 @@ def type_check(subtyping, env_updated_at)
source: source,
timestamp: Time.now
)
rescue => exn
@status = TypeCheckErrorStatus.new(error: exn)
end

true
Expand Down
128 changes: 128 additions & 0 deletions lib/steep/project/hover_content.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
module Steep
class Project
class HoverContent
TypeContent = Struct.new(:node, :type, :location, keyword_init: true)
VariableContent = Struct.new(:node, :name, :type, :location, keyword_init: true)
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)

InstanceMethodName = Struct.new(:class_name, :method_name)
SingletonMethodName = Struct.new(:class_name, :method_name)

attr_reader :project

def initialize(project:)
@project = project
end

def method_definition_for(factory, module_name, singleton_method: nil, instance_method: nil)
type_name = factory.type_name_1(module_name)

case
when instance_method
factory.definition_builder.build_instance(type_name).methods[instance_method]
when singleton_method
methods = factory.definition_builder.build_singleton(type_name).methods

if singleton_method == :new
methods[:new] || methods[:initialize]
else
methods[singleton_method]
end
end
end

def content_for(path:, line:, column:)
source_file = project.targets.map {|target| target.source_files[path] }.compact[0]

if source_file
case (status = source_file.status)
when SourceFile::TypeCheckStatus
node, *parents = status.source.find_nodes(line: line, column: column)

if node
case node.type
when :lvar, :lvasgn
var_name = node.children[0]
context = status.typing.context_of(node: node)
var_type = context.type_env.get(lvar: var_name.name)

VariableContent.new(node: node, name: var_name.name, type: var_type, location: node.location.name)
when :send
receiver, method_name, *_ = node.children


result_node = if parents[0]&.type == :block
parents[0]
else
node
end

context = status.typing.context_of(node: result_node)

receiver_type = if receiver
status.typing.type_of(node: receiver)
else
context.self_type
end

factory = context.type_env.subtyping.factory
method_name, definition = case receiver_type
when AST::Types::Name::Instance
method_definition = method_definition_for(factory, receiver_type.name, instance_method: method_name)
if method_definition&.defined_in
owner_name = factory.type_name(method_definition.defined_in.name.absolute!)
[
InstanceMethodName.new(owner_name, method_name),
method_definition
]
end
when AST::Types::Name::Class
method_definition = method_definition_for(factory, receiver_type.name, singleton_method: method_name)
if method_definition&.defined_in
owner_name = factory.type_name(method_definition.defined_in.name.absolute!)
[
SingletonMethodName.new(owner_name, method_name),
method_definition
]
end
else
nil
end

MethodCallContent.new(
node: node,
method_name: method_name,
type: status.typing.type_of(node: result_node),
definition: definition,
location: result_node.location.expression
)
when :def, :defs
context = status.typing.context_of(node: node)
method_context = context.method_context

if method_context
DefinitionContent.new(
node: node,
method_name: method_context.name,
method_type: method_context.method_type,
definition: method_context.method,
location: node.loc.expression
)
end
else
type = status.typing.type_of(node: node)

TypeContent.new(
node: node,
type: type,
location: node.location.expression
)
end
end
end
end
end
end
end
end
12 changes: 5 additions & 7 deletions lib/steep/source.rb
Original file line number Diff line number Diff line change
Expand Up @@ -294,8 +294,7 @@ def each_annotation
end
end

# @type method find_node: (line: Integer, column: Integer, ?node: any, ?position: Integer?) -> any
def find_node(line:, column:, node: self.node, position: nil)
def find_nodes(line:, column:, node: self.node, position: nil, parents: [])
position ||= (line-1).times.sum do |i|
node.location.expression.source_buffer.source_line(i+1).size + 1
end + column
Expand All @@ -306,14 +305,13 @@ def find_node(line:, column:, node: self.node, position: nil)

if range
if range === position
parents.unshift node

Source.each_child_node(node) do |child|
n = find_node(line: line, column: column, node: child, position: position)
if n
return n
end
ns = find_nodes(line: line, column: column, node: child, position: position, parents: parents) and return ns
end

node
parents
end
end
end
Expand Down
Loading