Skip to content

Commit 58f33ee

Browse files
committed
add lib
1 parent 35bb784 commit 58f33ee

7 files changed

Lines changed: 2376 additions & 3 deletions

File tree

lib/repl_completion.rb

Lines changed: 147 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,152 @@
11
# frozen_string_literal: true
22

3-
require_relative "repl_completion/version"
3+
require_relative 'repl_completion/version'
4+
require_relative 'repl_completion/type_analyzer'
5+
require_relative 'repl_completion/result'
46

57
module ReplCompletion
6-
class Error < StandardError; end
7-
# Your code goes here...
8+
class << self
9+
attr_reader :last_analyze_error
10+
11+
def rbs_load_error
12+
Types.rbs_load_error
13+
end
14+
15+
def rbs_load_started?
16+
Types.rbs_load_started?
17+
end
18+
19+
def rbs_loaded?
20+
!!Types.rbs_builder
21+
end
22+
23+
def load_rbs
24+
Types.load_rbs_builder unless rbs_loaded?
25+
end
26+
27+
def preload_rbs
28+
Types.preload_rbs_builder
29+
end
30+
31+
def analyze(code, binding)
32+
preload_rbs
33+
begin
34+
verbose, $VERBOSE = $VERBOSE, nil
35+
result = analyze_code(code, binding)
36+
rescue Exception => e
37+
handle_error(e)
38+
ensure
39+
$VERBOSE = verbose
40+
end
41+
Result.new(result, binding) if result
42+
end
43+
44+
def handle_error(e)
45+
@last_analyze_error = e
46+
end
47+
48+
def info
49+
require 'rbs'
50+
prism_info = "Prism: #{Prism::VERSION}"
51+
rbs_info = "RBS: #{RBS::VERSION}"
52+
if rbs_load_error
53+
rbs_info << " #{rbs_load_error.inspect}"
54+
elsif !rbs_load_started?
55+
rbs_info << ' signatures not loaded'
56+
elsif !rbs_loaded?
57+
rbs_info << ' signatures loading'
58+
end
59+
"ReplCompletion: #{VERSION}, #{prism_info}, #{rbs_info}"
60+
end
61+
62+
private
63+
64+
def analyze_code(code, binding = Object::TOPLEVEL_BINDING)
65+
# Workaround for https://github.com/ruby/prism/issues/1592
66+
return if code.match?(/%[qQ]\z/)
67+
68+
ast = Prism.parse(code, scopes: [binding.local_variables]).value
69+
name = code[/(@@|@|\$)?\w*[!?=]?\z/]
70+
*parents, target_node = find_target ast, code.bytesize - name.bytesize
71+
return unless target_node
72+
73+
calculate_scope = -> { TypeAnalyzer.calculate_target_type_scope(binding, parents, target_node).last }
74+
calculate_type_scope = ->(node) { TypeAnalyzer.calculate_target_type_scope binding, [*parents, target_node], node }
75+
76+
case target_node
77+
when Prism::StringNode, Prism::InterpolatedStringNode
78+
call_node, args_node = parents.last(2)
79+
return unless call_node.is_a?(Prism::CallNode) && call_node.receiver.nil?
80+
return unless args_node.is_a?(Prism::ArgumentsNode) && args_node.arguments.size == 1
81+
82+
case call_node.name
83+
when :require
84+
[:require, name]
85+
when :require_relative
86+
[:require_relative, name]
87+
end
88+
when Prism::SymbolNode
89+
if parents.last.is_a? Prism::BlockArgumentNode # method(&:target)
90+
receiver_type, _scope = calculate_type_scope.call target_node
91+
[:call, name, receiver_type, false]
92+
else
93+
[:symbol, name] unless name.empty?
94+
end
95+
when Prism::CallNode
96+
return [:lvar_or_method, name, calculate_scope.call] if target_node.receiver.nil?
97+
98+
self_call = target_node.receiver.is_a? Prism::SelfNode
99+
op = target_node.call_operator
100+
receiver_type, _scope = calculate_type_scope.call target_node.receiver
101+
receiver_type = receiver_type.nonnillable if op == '&.'
102+
[op == '::' ? :call_or_const : :call, name, receiver_type, self_call]
103+
when Prism::LocalVariableReadNode, Prism::LocalVariableTargetNode
104+
[:lvar_or_method, name, calculate_scope.call]
105+
when Prism::ConstantReadNode, Prism::ConstantTargetNode
106+
if parents.last.is_a? Prism::ConstantPathNode
107+
path_node = parents.last
108+
if path_node.parent # A::B
109+
receiver, scope = calculate_type_scope.call(path_node.parent)
110+
[:const, name, receiver, scope]
111+
else # ::A
112+
scope = calculate_scope.call
113+
[:const, name, Types::SingletonType.new(Object), scope]
114+
end
115+
else
116+
[:const, name, nil, calculate_scope.call]
117+
end
118+
when Prism::GlobalVariableReadNode, Prism::GlobalVariableTargetNode
119+
[:gvar, name, calculate_scope.call]
120+
when Prism::InstanceVariableReadNode, Prism::InstanceVariableTargetNode
121+
[:ivar, name, calculate_scope.call]
122+
when Prism::ClassVariableReadNode, Prism::ClassVariableTargetNode
123+
[:cvar, name, calculate_scope.call]
124+
end
125+
end
126+
127+
def find_target(node, position)
128+
location = (
129+
case node
130+
when Prism::CallNode
131+
node.message_loc
132+
when Prism::SymbolNode
133+
node.value_loc
134+
when Prism::StringNode
135+
node.content_loc
136+
when Prism::InterpolatedStringNode
137+
node.closing_loc if node.parts.empty?
138+
end
139+
)
140+
return [node] if location&.start_offset == position
141+
142+
node.compact_child_nodes.each do |n|
143+
match = find_target(n, position)
144+
next unless match
145+
match.unshift node
146+
return match
147+
end
148+
149+
[node] if node.location.start_offset == position
150+
end
151+
end
8152
end

lib/repl_completion/methods.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# frozen_string_literal: true
2+
3+
module ReplCompletion
4+
module Methods
5+
OBJECT_SINGLETON_CLASS_METHOD = Object.instance_method(:singleton_class)
6+
OBJECT_INSTANCE_VARIABLES_METHOD = Object.instance_method(:instance_variables)
7+
OBJECT_INSTANCE_VARIABLE_GET_METHOD = Object.instance_method(:instance_variable_get)
8+
OBJECT_CLASS_METHOD = Object.instance_method(:class)
9+
MODULE_NAME_METHOD = Module.instance_method(:name)
10+
end
11+
end
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# frozen_string_literal: true
2+
3+
module ReplCompletion
4+
module RequirePaths
5+
class << self
6+
def require_completions(target_path)
7+
*dir, target = target_path.split('/', -1)
8+
target ||= ''
9+
paths = with_cache [:require_completions, dir] do
10+
gem_and_system_load_paths.flat_map do |load_path|
11+
base_dir = File.absolute_path(File.join(load_path, *dir))
12+
with_cache [:requireable_paths, base_dir] do
13+
requireable_paths(base_dir)
14+
end
15+
end.sort
16+
end
17+
paths.filter_map do |path|
18+
[*dir, path].join('/') if path.start_with?(target)
19+
end
20+
end
21+
22+
def require_relative_completions(target_path, binding)
23+
current_filepath = binding.source_location[0]
24+
*dir, target = target_path.split('/', -1)
25+
target ||= ''
26+
base_dir = File.absolute_path(File.join(File.dirname(current_filepath), *dir))
27+
paths = with_cache [:requireable_paths, base_dir] do
28+
requireable_paths(base_dir)
29+
end
30+
paths.filter_map do |path|
31+
[*dir, path].join('/') if path.start_with?(target)
32+
end
33+
end
34+
35+
private
36+
37+
def with_cache(key)
38+
@cache ||= {}
39+
@cache[key] ||= yield
40+
end
41+
42+
def gem_paths
43+
return [] unless defined?(Gem::Specification)
44+
45+
Gem::Specification.latest_specs(true).flat_map do |spec|
46+
spec.require_paths.map do |path|
47+
File.absolute_path?(path) ? path : File.join(spec.full_gem_path, path)
48+
end
49+
end
50+
end
51+
52+
def gem_and_system_load_paths
53+
@gem_and_system_load_paths ||= (gem_paths | $LOAD_PATH).filter_map do |path|
54+
if path.respond_to?(:to_path)
55+
path.to_path
56+
else
57+
String(path) rescue nil
58+
end
59+
end.sort
60+
end
61+
62+
def requireable_paths(base_dir)
63+
ext = ".{rb,#{RbConfig::CONFIG['DLEXT']}}"
64+
ext_regexp = /\.(rb|#{RbConfig::CONFIG['DLEXT']})\z/
65+
files = Dir.glob(["*#{ext}", "*/*#{ext}"], base: base_dir).map { |p| p.sub(ext_regexp, '') }
66+
dirs = Dir.glob('*/*', base: base_dir).filter_map do |path|
67+
"#{path}/" if File.directory?(File.join(base_dir, path))
68+
end
69+
(files + dirs).sort
70+
rescue Errno::EPERM
71+
[]
72+
end
73+
end
74+
end
75+
end

lib/repl_completion/result.rb

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# frozen_string_literal: true
2+
3+
require_relative 'require_paths'
4+
5+
module ReplCompletion
6+
class Result
7+
HIDDEN_METHODS = %w[Namespace TypeName] # defined by rbs, should be hidden
8+
RESERVED_WORDS = %w[
9+
__ENCODING__ __LINE__ __FILE__
10+
BEGIN END
11+
alias and
12+
begin break
13+
case class
14+
def defined? do
15+
else elsif end ensure
16+
false for
17+
if in
18+
module
19+
next nil not
20+
or
21+
redo rescue retry return
22+
self super
23+
then true
24+
undef unless until
25+
when while
26+
yield
27+
]
28+
29+
def initialize(analyze_result, binding)
30+
@analyze_result = analyze_result
31+
@binding = binding
32+
end
33+
34+
def completion_candidates
35+
verbose, $VERBOSE = $VERBOSE, nil
36+
candidates = case @analyze_result
37+
in [:require, name]
38+
RequirePaths.require_completions(name)
39+
in [:require_relative, name]
40+
RequirePaths.require_relative_completions(name, @binding)
41+
in [:call_or_const, name, type, self_call]
42+
((self_call ? type.all_methods : type.methods).map(&:to_s) - HIDDEN_METHODS) | type.constants
43+
in [:const, name, type, scope]
44+
if type
45+
scope_constants = type.types.flat_map do |t|
46+
scope.table_module_constants(t.module_or_class) if t.is_a?(Types::SingletonType)
47+
end
48+
(scope_constants.compact | type.constants.map(&:to_s)).sort
49+
else
50+
scope.constants.sort | RESERVED_WORDS
51+
end
52+
in [:ivar, name, scope]
53+
ivars = scope.instance_variables.sort
54+
name == '@' ? ivars + scope.class_variables.sort : ivars
55+
in [:cvar, name, scope]
56+
scope.class_variables
57+
in [:gvar, name, scope]
58+
scope.global_variables
59+
in [:symbol, name]
60+
Symbol.all_symbols.map { _1.inspect[1..] }
61+
in [:call, name, type, self_call]
62+
(self_call ? type.all_methods : type.methods).map(&:to_s) - HIDDEN_METHODS
63+
in [:lvar_or_method, name, scope]
64+
scope.self_type.all_methods.map(&:to_s) | scope.local_variables | RESERVED_WORDS
65+
else
66+
[]
67+
end
68+
candidates.select { _1.start_with?(name) }.map { _1[name.size..] }
69+
ensure
70+
$VERBOSE = verbose
71+
end
72+
73+
def doc_namespace(matched)
74+
verbose, $VERBOSE = $VERBOSE, nil
75+
case @analyze_result
76+
in [:call_or_const, prefix, type, _self_call]
77+
call_or_const_doc type, prefix + matched
78+
in [:const, prefix, type, scope]
79+
if type
80+
call_or_const_doc type, prefix + matched
81+
else
82+
value_doc scope[prefix + matched]
83+
end
84+
in [:gvar, prefix, scope]
85+
value_doc scope[prefix + matched]
86+
in [:ivar, prefix, scope]
87+
value_doc scope[prefix + matched]
88+
in [:cvar, prefix, scope]
89+
value_doc scope[prefix + matched]
90+
in [:call, prefix, type, _self_call]
91+
method_doc type, prefix + matched
92+
in [:lvar_or_method, prefix, scope]
93+
if scope.local_variables.include?(prefix + matched)
94+
value_doc scope[prefix + matched]
95+
else
96+
method_doc scope.self_type, prefix + matched
97+
end
98+
else
99+
end
100+
ensure
101+
$VERBOSE = verbose
102+
end
103+
104+
private
105+
106+
def method_doc(type, name)
107+
type = type.types.find { _1.all_methods.include? name.to_sym }
108+
case type
109+
when Types::SingletonType
110+
"#{Types.class_name_of(type.module_or_class)}.#{name}"
111+
when Types::InstanceType
112+
"#{Types.class_name_of(type.klass)}##{name}"
113+
end
114+
end
115+
116+
def call_or_const_doc(type, name)
117+
if name =~ /\A[A-Z]/
118+
type = type.types.grep(Types::SingletonType).find { _1.module_or_class.const_defined?(name) }
119+
type.module_or_class == Object ? name : "#{Types.class_name_of(type.module_or_class)}::#{name}" if type
120+
else
121+
method_doc(type, name)
122+
end
123+
end
124+
125+
def value_doc(type)
126+
return unless type
127+
type.types.each do |t|
128+
case t
129+
when Types::SingletonType
130+
return Types.class_name_of(t.module_or_class)
131+
when Types::InstanceType
132+
return Types.class_name_of(t.klass)
133+
end
134+
end
135+
nil
136+
end
137+
end
138+
end

0 commit comments

Comments
 (0)