Skip to content

Commit 1048c7e

Browse files
authored
Type based completion using Prism and RBS (#708)
* Add completor using prism and rbs * Add TypeCompletion test * Switchable completors: RegexpCompletor and TypeCompletion::Completor * Add completion info to irb_info * Complete reserved words * Fix [*] (*) {**} and prism's change of KeywordParameterNode * Fix require, frozen_string_literal * Drop prism<=0.16.0 support * Add Completor.last_completion_error for debug report * Retrieve `self` and `Module.nesting` in more safe way * Support BasicObject * Handle lvar and ivar get exception correctly * Skip ivar reference test of non-self object in ruby < 3.2 * BaseScope to RootScope, move method objects constant under Methods * Remove unused Splat struct * Drop deeply nested array/hash type calculation from actual object. Now, calculation depth is 1 * Refactor loading rbs in test, change preload_in_thread not to cache Thread object * Use new option added in prism 0.17.1 to parse code with localvars * Add Prism version check and warn when :type completor cannot be enabled * build_type_completor should skip truffleruby (because endless method definition is not supported)
1 parent 3a28eee commit 1048c7e

File tree

20 files changed

+3453
-34
lines changed

20 files changed

+3453
-34
lines changed

Gemfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,8 @@ gem "test-unit-ruby-core"
1919
gem "debug", github: "ruby/debug"
2020

2121
gem "racc"
22+
23+
if RUBY_VERSION >= "3.0.0"
24+
gem "rbs"
25+
gem "prism", ">= 0.17.1"
26+
end

Rakefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@ Rake::TestTask.new(:test) do |t|
55
t.libs << "test" << "test/lib"
66
t.libs << "lib"
77
t.ruby_opts << "-rhelper"
8-
t.test_files = FileList["test/irb/test_*.rb"]
8+
t.test_files = FileList["test/irb/test_*.rb", "test/irb/type_completion/test_*.rb"]
99
end
1010

1111
# To make sure they have been correctly setup for Ruby CI.
1212
desc "Run each irb test file in isolation."
1313
task :test_in_isolation do
1414
failed = false
1515

16-
FileList["test/irb/test_*.rb"].each do |test_file|
16+
FileList["test/irb/test_*.rb", "test/irb/type_completion/test_*.rb"].each do |test_file|
1717
ENV["TEST"] = test_file
1818
begin
1919
Rake::Task["test"].execute

lib/irb.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@
140140
#
141141
# IRB.conf[:USE_AUTOCOMPLETE] = false
142142
#
143+
# To enable enhanced completion using type information, add the following to your +.irbrc+:
144+
#
145+
# IRB.conf[:COMPLETOR] = :type
146+
#
143147
# === History
144148
#
145149
# By default, irb will store the last 1000 commands you used in

lib/irb/cmd/irb_info.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ def execute
1414
str = "Ruby version: #{RUBY_VERSION}\n"
1515
str += "IRB version: #{IRB.version}\n"
1616
str += "InputMethod: #{IRB.CurrentContext.io.inspect}\n"
17+
str += "Completion: #{IRB.CurrentContext.io.respond_to?(:completion_info) ? IRB.CurrentContext.io.completion_info : 'off'}\n"
1718
str += ".irbrc path: #{IRB.rc_file}\n" if File.exist?(IRB.rc_file)
1819
str += "RUBY_PLATFORM: #{RUBY_PLATFORM}\n"
1920
str += "LANG env: #{ENV["LANG"]}\n" if ENV["LANG"] && !ENV["LANG"].empty?

lib/irb/completion.rb

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,30 @@
99

1010
module IRB
1111
class BaseCompletor # :nodoc:
12+
13+
# Set of reserved words used by Ruby, you should not use these for
14+
# constants or variables
15+
ReservedWords = %w[
16+
__ENCODING__ __LINE__ __FILE__
17+
BEGIN END
18+
alias and
19+
begin break
20+
case class
21+
def defined? do
22+
else elsif end ensure
23+
false for
24+
if in
25+
module
26+
next nil not
27+
or
28+
redo rescue retry return
29+
self super
30+
then true
31+
undef unless until
32+
when while
33+
yield
34+
]
35+
1236
def completion_candidates(preposing, target, postposing, bind:)
1337
raise NotImplementedError
1438
end
@@ -94,28 +118,9 @@ def eval_class_constants
94118
end
95119
}
96120

97-
# Set of reserved words used by Ruby, you should not use these for
98-
# constants or variables
99-
ReservedWords = %w[
100-
__ENCODING__ __LINE__ __FILE__
101-
BEGIN END
102-
alias and
103-
begin break
104-
case class
105-
def defined? do
106-
else elsif end ensure
107-
false for
108-
if in
109-
module
110-
next nil not
111-
or
112-
redo rescue retry return
113-
self super
114-
then true
115-
undef unless until
116-
when while
117-
yield
118-
]
121+
def inspect
122+
'RegexpCompletor'
123+
end
119124

120125
def complete_require_path(target, preposing, postposing)
121126
if target =~ /\A(['"])([^'"]+)\Z/

lib/irb/context.rb

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,14 @@ def initialize(irb, workspace = nil, input_method = nil)
8686
when nil
8787
if STDIN.tty? && IRB.conf[:PROMPT_MODE] != :INF_RUBY && !use_singleline?
8888
# Both of multiline mode and singleline mode aren't specified.
89-
@io = RelineInputMethod.new
89+
@io = RelineInputMethod.new(build_completor)
9090
else
9191
@io = nil
9292
end
9393
when false
9494
@io = nil
9595
when true
96-
@io = RelineInputMethod.new
96+
@io = RelineInputMethod.new(build_completor)
9797
end
9898
unless @io
9999
case use_singleline?
@@ -149,6 +149,43 @@ def initialize(irb, workspace = nil, input_method = nil)
149149
@command_aliases = IRB.conf[:COMMAND_ALIASES]
150150
end
151151

152+
private def build_completor
153+
completor_type = IRB.conf[:COMPLETOR]
154+
case completor_type
155+
when :regexp
156+
return RegexpCompletor.new
157+
when :type
158+
completor = build_type_completor
159+
return completor if completor
160+
else
161+
warn "Invalid value for IRB.conf[:COMPLETOR]: #{completor_type}"
162+
end
163+
# Fallback to RegexpCompletor
164+
RegexpCompletor.new
165+
end
166+
167+
TYPE_COMPLETION_REQUIRED_PRISM_VERSION = '0.17.1'
168+
169+
private def build_type_completor
170+
unless Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.0.0') && RUBY_ENGINE != 'truffleruby'
171+
warn 'TypeCompletion requires RUBY_VERSION >= 3.0.0'
172+
return
173+
end
174+
begin
175+
require 'prism'
176+
rescue LoadError => e
177+
warn "TypeCompletion requires Prism: #{e.message}"
178+
return
179+
end
180+
unless Gem::Version.new(Prism::VERSION) >= Gem::Version.new(TYPE_COMPLETION_REQUIRED_PRISM_VERSION)
181+
warn "TypeCompletion requires Prism::VERSION >= #{TYPE_COMPLETION_REQUIRED_PRISM_VERSION}"
182+
return
183+
end
184+
require 'irb/type_completion/completor'
185+
TypeCompletion::Types.preload_in_thread
186+
TypeCompletion::Completor.new
187+
end
188+
152189
def save_history=(val)
153190
IRB.conf[:SAVE_HISTORY] = val
154191
end

lib/irb/init.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ def IRB.init_config(ap_path)
7676
@CONF[:USE_SINGLELINE] = false unless defined?(ReadlineInputMethod)
7777
@CONF[:USE_COLORIZE] = (nc = ENV['NO_COLOR']).nil? || nc.empty?
7878
@CONF[:USE_AUTOCOMPLETE] = ENV.fetch("IRB_USE_AUTOCOMPLETE", "true") != "false"
79+
@CONF[:COMPLETOR] = :regexp
7980
@CONF[:INSPECT_MODE] = true
8081
@CONF[:USE_TRACER] = false
8182
@CONF[:USE_LOADER] = false

lib/irb/input-method.rb

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,10 @@ def initialize
193193
}
194194
end
195195

196+
def completion_info
197+
'RegexpCompletor'
198+
end
199+
196200
# Reads the next line from this input method.
197201
#
198202
# See IO#gets for more information.
@@ -230,13 +234,13 @@ class RelineInputMethod < StdioInputMethod
230234
HISTORY = Reline::HISTORY
231235
include HistorySavingAbility
232236
# Creates a new input method object using Reline
233-
def initialize
237+
def initialize(completor)
234238
IRB.__send__(:set_encoding, Reline.encoding_system_needs.name, override: false)
235239

236-
super
240+
super()
237241

238242
@eof = false
239-
@completor = RegexpCompletor.new
243+
@completor = completor
240244

241245
Reline.basic_word_break_characters = BASIC_WORD_BREAK_CHARACTERS
242246
Reline.completion_append_character = nil
@@ -270,6 +274,11 @@ def initialize
270274
end
271275
end
272276

277+
def completion_info
278+
autocomplete_message = Reline.autocompletion ? 'Autocomplete' : 'Tab Complete'
279+
"#{autocomplete_message}, #{@completor.inspect}"
280+
end
281+
273282
def check_termination(&block)
274283
@check_termination_proc = block
275284
end

0 commit comments

Comments
 (0)