Skip to content

Commit

Permalink
Merge pull request #117 from soutaro/hover2
Browse files Browse the repository at this point in the history
Improve hover
  • Loading branch information
soutaro authored Feb 16, 2020
2 parents dd3c819 + 7f42b47 commit 5e9de8b
Show file tree
Hide file tree
Showing 15 changed files with 951 additions and 308 deletions.
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

0 comments on commit 5e9de8b

Please sign in to comment.