lib/rouge/repl.rb in rouge-lang-0.0.11 vs lib/rouge/repl.rb in rouge-lang-0.0.12
- old
+ new
@@ -15,12 +15,14 @@
end
context = Rouge::Context.new(Rouge[:user])
count = 0
chaining = false
+ Readline.completion_proc = Completer.new(context.ns)
while true
+
if !chaining
prompt = "#{context.ns.name}=> "
input = Readline.readline(prompt, true)
else
prompt = "#{" " * [0, context.ns.name.length - 2].max}#_=> "
@@ -38,23 +40,21 @@
next
rescue Rouge::Reader::EndOfDataError
chaining = true
next
rescue Rouge::Reader::UnexpectedCharacterError => reader_err
- repl_error.call reader_err
+ repl_error.call(reader_err)
rescue Rouge::Reader::NumberFormatError => reader_err
- repl_error.call reader_err
+ repl_error.call(reader_err)
end
chaining = false
begin
- form = Rouge::Compiler.compile(
- context.ns,
- Set[*context.lexical_keys],
- form
- )
+ form = Rouge::Compiler.compile(context.ns,
+ Set[*context.lexical_keys],
+ form)
result = context.eval(form)
Rouge.print(result, STDOUT)
STDOUT.puts
@@ -64,15 +64,255 @@
context.set_here :"*#{i}", context[:"*#{i - 1}"]
end
context.set_here :"*1", result
rescue Rouge::Context::ChangeContextException => cce
context = cce.context
+ # Since completion is context sensitive, we need update the proc
+ # whenever it changes.
+ Readline.completion_proc = Completer.new(context.ns)
count = 0
rescue => e
- repl_error.call e
+ repl_error.call(e)
end
end
end
+ module Completer
+ extend self
+
+ # Returns a proc intended to be used with Readline.
+ #
+ # @param [Rouge::Namespace] current_namespace
+ # the current namespace
+ #
+ # @return [Proc] the completion proc to be used with Readline. The
+ # returned proc accepts a string and returns an array.
+ #
+ # @api public
+ def new(current_namespace)
+ return lambda do |query|
+ if query.nil? || query.empty?
+ return []
+ end
+
+ list = namespace_items(current_namespace)
+ list << search(query)
+
+ matches = search_list(list.flatten, query)
+
+ # If there's only one match we check if it's a namespace or a Ruby
+ # constant which contains other constants or singleton methods.
+ if matches.length == 1
+ match = matches[0]
+ if Rouge::Namespace.exists?(match)
+ if current_namespace.table.include?(match)
+ matches << "#{match}/"
+ else
+ Readline.completion_append_character = "/"
+ end
+ else
+ if locate_module(match.to_s)
+ Readline.completion_append_character = ""
+ end
+ end
+ else
+ Readline.completion_append_character = ""
+ end
+
+ matches
+ end
+ end
+
+ # Returns a list of constants and singleton method names based on the string
+ # query.
+ #
+ # @param [String] query
+ # the search string to use
+ #
+ # @return [Array<Symbol,String>] the search results
+ #
+ # @api public
+ def search(query)
+ namespace, lookup = query.split('/', 2)
+ result =
+ case namespace
+ # The ruby namespace requires special handling.
+ when /^[A-Z]/
+ search_ruby(query)
+ when /^ruby/
+ if lookup && lookup.empty?
+ Rouge[:ruby].table.map {|x| "ruby/#{x}" }
+ else
+ search_ruby(lookup).map {|x| "ruby/#{x}" }
+ end
+ else
+ ns = rg_namespaces[namespace.to_sym]
+
+ if ns
+ ns.table.map { |var, _| "#{namespace}/#{var}" }
+ else
+ # Add the current namepace, rouge.builtin, and ruby tables along with
+ # the names of available namespaces in the completion list.
+ list = []
+ list << Rouge[:"rouge.builtin"].table.keys
+ list << :ruby
+ list << Rouge[:ruby].table
+ list << rg_namespaces.keys
+ end
+ end
+
+ search_list(result.flatten, query)
+ end
+
+ # Applies `locate_module` to the string query and returns a list constants
+ # and singleton methods. These results are intended to be filtered in the
+ # `search` method.
+ #
+ # @see Completer.locate_module, Completer.search
+ #
+ # @example
+ # search_ruby("Rouge") #=> ["Rouge/[]", "Rouge/boot!", ...]
+ # search_ruby("Rouge.") #=> ["Rouge/[]", "Rouge/boot!", ...]
+ #
+ # @param [String] query
+ # the search string to use
+ #
+ # @return [Array<Symbol,String>] the search result
+ #
+ # @api public
+ def search_ruby(query)
+ namespace = query.split('/', 2).first
+
+ mod = locate_module(namespace)
+
+ if mod == Object
+ mod.constants
+ else
+ ns = mod.name.gsub('::','.')
+ result = []
+ mod.singleton_methods.each { |sm| result << "#{ns}/#{sm}" }
+ mod.constants.each { |c| result << "#{ns}.#{c}" }
+ result.flatten
+ end
+ end
+
+ # Recursively searches for a Ruby module (includes classes) given by the
+ # string query. The string should contain a Rouge style namespace name for
+ # the module.
+ #
+ # Optionally, a root module can be supplied as the context for the query.
+ # By default this is Object. If no module is found, the method returns nil
+ # or the root.
+ #
+ # Be aware this method *only* returns modules and classes.
+ #
+ # @example
+ # locate_module("Bil.Bo") #=> Bil::Bo
+ # locate_module("Ji.Tsu", Nin) #=> Nin::Ji::Tsu
+ #
+ # @param [String] query
+ # the module (or class) to find
+ #
+ # @param [Module] root
+ # the optional search context
+ #
+ # @return [Class,Module,nil] the search result
+ #
+ # @api public
+ def locate_module(query, root = Object)
+ head, tail = query.split('.', 2)
+
+ return root unless rg_ruby_module?(head)
+
+ lookup = head.to_sym
+
+ if root.is_a?(Module) && root.constants.include?(lookup)
+ result = root.const_get(lookup)
+
+ return root unless result.is_a?(Module)
+
+ # `query` may have ended with '.'.
+ if tail.nil? || tail.empty?
+ result
+ else
+ locate_module(tail, result)
+ end
+ else
+ root
+ end
+ end
+
+ # Rouge namespaces. Note we do not include the rouge.builtin and ruby
+ # namespaces since we would like built in vars, such as def or let, and top
+ # level Ruby constants to be easily accessible with command line
+ # completion.
+ #
+ # @return [Hash] the filtered namespaces
+ #
+ # @api public
+ def rg_namespaces
+ Rouge::Namespace.all.reject do |key, _|
+ [:"rouge.builtin", :ruby].include?(key)
+ end
+ end
+
+ # Returns true if the provided namespace is included in `rg_namespaces`.
+ #
+ # @see Rouge::REPL::Completer.rg_namespaces
+ #
+ # @param [Rouge::Namespace] x
+ # the namespace to test
+ #
+ # @return [Boolean]
+ #
+ # @api public
+ def rg_namespace?(x)
+ x.is_a?(Rouge::Namespace) && rg_namespaces.keys.include?(x.name)
+ end
+
+ private
+
+ # Returns a list of table keys and refer keys based on the given namespace.
+ #
+ # @param [Rouge::Namespace] ns
+ # the namespace to use
+ #
+ # @return [Array<Symbol>]
+ #
+ # @api public
+ def namespace_items(ns)
+ list = ns.table.keys
+ refers = ns.refers.select { |ns| rg_namespace?(ns) }
+ list << refers.map { |ns| ns.table.keys }
+ end
+
+ # Returns true if the string query matches a Rouge style Ruby module or
+ # constant name.
+ #
+ # @param [String] query
+ # the query string to match.
+ #
+ # @return [Boolean]
+ #
+ # @api private
+ def rg_ruby_module?(x)
+ !!/^(?:[A-Z][A-Za-z_]*\.?)+$/.match(x)
+ end
+
+ # Filters a list of items based on a string query.
+ #
+ # @param [Array] list
+ # the list to filter.
+ #
+ # @param [String] query
+ # the search string to use.
+ #
+ # @return [Array]
+ #
+ # @api private
+ def search_list(list, query)
+ list.grep(/^#{Regexp.escape(query)}/)
+ end
+ end
end
# vim: set sw=2 et cc=80: