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: