Skip to content

Commit

Permalink
Type based completion using Prism and RBS (#708)
Browse files Browse the repository at this point in the history
* 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)
  • Loading branch information
tompng authored Nov 8, 2023
1 parent 3a28eee commit 1048c7e
Show file tree
Hide file tree
Showing 20 changed files with 3,453 additions and 34 deletions.
5 changes: 5 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,8 @@ gem "test-unit-ruby-core"
gem "debug", github: "ruby/debug"

gem "racc"

if RUBY_VERSION >= "3.0.0"
gem "rbs"
gem "prism", ">= 0.17.1"
end
4 changes: 2 additions & 2 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ Rake::TestTask.new(:test) do |t|
t.libs << "test" << "test/lib"
t.libs << "lib"
t.ruby_opts << "-rhelper"
t.test_files = FileList["test/irb/test_*.rb"]
t.test_files = FileList["test/irb/test_*.rb", "test/irb/type_completion/test_*.rb"]
end

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

FileList["test/irb/test_*.rb"].each do |test_file|
FileList["test/irb/test_*.rb", "test/irb/type_completion/test_*.rb"].each do |test_file|
ENV["TEST"] = test_file
begin
Rake::Task["test"].execute
Expand Down
4 changes: 4 additions & 0 deletions lib/irb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@
#
# IRB.conf[:USE_AUTOCOMPLETE] = false
#
# To enable enhanced completion using type information, add the following to your +.irbrc+:
#
# IRB.conf[:COMPLETOR] = :type
#
# === History
#
# By default, irb will store the last 1000 commands you used in
Expand Down
1 change: 1 addition & 0 deletions lib/irb/cmd/irb_info.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ def execute
str = "Ruby version: #{RUBY_VERSION}\n"
str += "IRB version: #{IRB.version}\n"
str += "InputMethod: #{IRB.CurrentContext.io.inspect}\n"
str += "Completion: #{IRB.CurrentContext.io.respond_to?(:completion_info) ? IRB.CurrentContext.io.completion_info : 'off'}\n"
str += ".irbrc path: #{IRB.rc_file}\n" if File.exist?(IRB.rc_file)
str += "RUBY_PLATFORM: #{RUBY_PLATFORM}\n"
str += "LANG env: #{ENV["LANG"]}\n" if ENV["LANG"] && !ENV["LANG"].empty?
Expand Down
49 changes: 27 additions & 22 deletions lib/irb/completion.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,30 @@

module IRB
class BaseCompletor # :nodoc:

# Set of reserved words used by Ruby, you should not use these for
# constants or variables
ReservedWords = %w[
__ENCODING__ __LINE__ __FILE__
BEGIN END
alias and
begin break
case class
def defined? do
else elsif end ensure
false for
if in
module
next nil not
or
redo rescue retry return
self super
then true
undef unless until
when while
yield
]

def completion_candidates(preposing, target, postposing, bind:)
raise NotImplementedError
end
Expand Down Expand Up @@ -94,28 +118,9 @@ def eval_class_constants
end
}

# Set of reserved words used by Ruby, you should not use these for
# constants or variables
ReservedWords = %w[
__ENCODING__ __LINE__ __FILE__
BEGIN END
alias and
begin break
case class
def defined? do
else elsif end ensure
false for
if in
module
next nil not
or
redo rescue retry return
self super
then true
undef unless until
when while
yield
]
def inspect
'RegexpCompletor'
end

def complete_require_path(target, preposing, postposing)
if target =~ /\A(['"])([^'"]+)\Z/
Expand Down
41 changes: 39 additions & 2 deletions lib/irb/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,14 @@ def initialize(irb, workspace = nil, input_method = nil)
when nil
if STDIN.tty? && IRB.conf[:PROMPT_MODE] != :INF_RUBY && !use_singleline?
# Both of multiline mode and singleline mode aren't specified.
@io = RelineInputMethod.new
@io = RelineInputMethod.new(build_completor)
else
@io = nil
end
when false
@io = nil
when true
@io = RelineInputMethod.new
@io = RelineInputMethod.new(build_completor)
end
unless @io
case use_singleline?
Expand Down Expand Up @@ -149,6 +149,43 @@ def initialize(irb, workspace = nil, input_method = nil)
@command_aliases = IRB.conf[:COMMAND_ALIASES]
end

private def build_completor
completor_type = IRB.conf[:COMPLETOR]
case completor_type
when :regexp
return RegexpCompletor.new
when :type
completor = build_type_completor
return completor if completor
else
warn "Invalid value for IRB.conf[:COMPLETOR]: #{completor_type}"
end
# Fallback to RegexpCompletor
RegexpCompletor.new
end

TYPE_COMPLETION_REQUIRED_PRISM_VERSION = '0.17.1'

private def build_type_completor
unless Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.0.0') && RUBY_ENGINE != 'truffleruby'
warn 'TypeCompletion requires RUBY_VERSION >= 3.0.0'
return
end
begin
require 'prism'
rescue LoadError => e
warn "TypeCompletion requires Prism: #{e.message}"
return
end
unless Gem::Version.new(Prism::VERSION) >= Gem::Version.new(TYPE_COMPLETION_REQUIRED_PRISM_VERSION)
warn "TypeCompletion requires Prism::VERSION >= #{TYPE_COMPLETION_REQUIRED_PRISM_VERSION}"
return
end
require 'irb/type_completion/completor'
TypeCompletion::Types.preload_in_thread
TypeCompletion::Completor.new
end

def save_history=(val)
IRB.conf[:SAVE_HISTORY] = val
end
Expand Down
1 change: 1 addition & 0 deletions lib/irb/init.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ def IRB.init_config(ap_path)
@CONF[:USE_SINGLELINE] = false unless defined?(ReadlineInputMethod)
@CONF[:USE_COLORIZE] = (nc = ENV['NO_COLOR']).nil? || nc.empty?
@CONF[:USE_AUTOCOMPLETE] = ENV.fetch("IRB_USE_AUTOCOMPLETE", "true") != "false"
@CONF[:COMPLETOR] = :regexp
@CONF[:INSPECT_MODE] = true
@CONF[:USE_TRACER] = false
@CONF[:USE_LOADER] = false
Expand Down
15 changes: 12 additions & 3 deletions lib/irb/input-method.rb
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,10 @@ def initialize
}
end

def completion_info
'RegexpCompletor'
end

# Reads the next line from this input method.
#
# See IO#gets for more information.
Expand Down Expand Up @@ -230,13 +234,13 @@ class RelineInputMethod < StdioInputMethod
HISTORY = Reline::HISTORY
include HistorySavingAbility
# Creates a new input method object using Reline
def initialize
def initialize(completor)
IRB.__send__(:set_encoding, Reline.encoding_system_needs.name, override: false)

super
super()

@eof = false
@completor = RegexpCompletor.new
@completor = completor

Reline.basic_word_break_characters = BASIC_WORD_BREAK_CHARACTERS
Reline.completion_append_character = nil
Expand Down Expand Up @@ -270,6 +274,11 @@ def initialize
end
end

def completion_info
autocomplete_message = Reline.autocompletion ? 'Autocomplete' : 'Tab Complete'
"#{autocomplete_message}, #{@completor.inspect}"
end

def check_termination(&block)
@check_termination_proc = block
end
Expand Down
Loading

0 comments on commit 1048c7e

Please sign in to comment.