ruby-changes:67292
From: aycabta <ko1@a...>
Date: Sun, 29 Aug 2021 20:30:36 +0900 (JST)
Subject: [ruby-changes:67292] fb0fc20196 (master): [ruby/reline] Implement dialog with autocomplete callback
https://git.ruby-lang.org/ruby.git/commit/?id=fb0fc20196 From fb0fc201963c5e70e62b72e0ac9e27dc39e0f5ec Mon Sep 17 00:00:00 2001 From: aycabta <aycabta@g...> Date: Tue, 17 Aug 2021 19:21:27 +0900 Subject: [ruby/reline] Implement dialog with autocomplete callback https://github.com/ruby/reline/commit/1401d6165e --- lib/reline.rb | 24 +++++ lib/reline/line_editor.rb | 238 +++++++++++++++++++++++++++++++++++++++++++++- lib/reline/unicode.rb | 30 ++++++ 3 files changed, 289 insertions(+), 3 deletions(-) diff --git a/lib/reline.rb b/lib/reline.rb index d68627d..fade2da 100644 --- a/lib/reline.rb +++ b/lib/reline.rb @@ -34,6 +34,7 @@ module Reline https://github.com/ruby/ruby/blob/trunk/lib/reline.rb#L34 auto_indent_proc pre_input_hook dig_perfect_match_proc + dialog_proc ).each(&method(:attr_reader)) attr_accessor :config @@ -130,6 +131,11 @@ module Reline https://github.com/ruby/ruby/blob/trunk/lib/reline.rb#L131 @dig_perfect_match_proc = p end + def dialog_proc=(p) + raise ArgumentError unless p.respond_to?(:call) or p.nil? + @dialog_proc = p + end + def input=(val) raise TypeError unless val.respond_to?(:getc) or val.nil? if val.respond_to?(:getc) @@ -175,6 +181,23 @@ module Reline https://github.com/ruby/ruby/blob/trunk/lib/reline.rb#L181 unless confirm_multiline_termination raise ArgumentError.new('#readmultiline needs block to confirm multiline termination') end + @dialog_proc = ->() { + # autocomplete + if just_cursor_moving + # Auto complete starts only when endited + return nil + end + pre, target, post= retrieve_completion_block(true) + if target.nil? or target.empty? + result = nil + else + result = call_completion_proc_with_checking_args(pre, target, post) + if result and result.size == 1 and result[0] == target + result = nil + end + end + [Reline::CursorPos.new(cursor_pos.x - Reline::Unicode.calculate_width(target), nil), result] + } inner_readline(prompt, add_hist, true, &confirm_multiline_termination) whole_buffer = line_editor.whole_buffer.dup @@ -230,6 +253,7 @@ module Reline https://github.com/ruby/ruby/blob/trunk/lib/reline.rb#L253 line_editor.auto_indent_proc = auto_indent_proc line_editor.dig_perfect_match_proc = dig_perfect_match_proc line_editor.pre_input_hook = pre_input_hook + line_editor.dialog_proc = dialog_proc unless config.test_mode config.read diff --git a/lib/reline/line_editor.rb b/lib/reline/line_editor.rb index 0b5b683..8e620e5 100644 --- a/lib/reline/line_editor.rb +++ b/lib/reline/line_editor.rb @@ -249,6 +249,13 @@ class Reline::LineEditor https://github.com/ruby/ruby/blob/trunk/lib/reline/line_editor.rb#L249 @drop_terminate_spaces = false @in_pasting = false @auto_indent_proc = nil + @with_dialog = nil + @dialog_column = nil + @dialog_vertical_offset = nil + @dialog_contents = nil + @dialog_contents_width = nil + @dialog_updown = nil + @dialog_lines_backup = nil reset_line end @@ -414,6 +421,7 @@ class Reline::LineEditor https://github.com/ruby/ruby/blob/trunk/lib/reline/line_editor.rb#L421 Reline::IOGate.erase_after_cursor end @output.flush + clear_dialog return end new_highest_in_this = calculate_height_by_width(prompt_width + calculate_width(@line.nil? ? '' : @line)) @@ -424,6 +432,7 @@ class Reline::LineEditor https://github.com/ruby/ruby/blob/trunk/lib/reline/line_editor.rb#L432 else if @just_cursor_moving and not @rerender_all rendered = just_move_cursor + render_dialog((prompt_width + @cursor) % @screen_size.last) @just_cursor_moving = false return elsif @previous_line_index or new_highest_in_this != @highest_in_this @@ -446,18 +455,20 @@ class Reline::LineEditor https://github.com/ruby/ruby/blob/trunk/lib/reline/line_editor.rb#L455 new_lines = whole_lines end line = modify_lines(new_lines)[@line_index] + clear_dialog prompt, prompt_width, prompt_list = check_multiline_prompt(new_lines, prompt) render_partial(prompt, prompt_width, line, @first_line_started_from) move_cursor_down(@highest_in_all - (@first_line_started_from + @highest_in_this - 1) - 1) scroll_down(1) Reline::IOGate.move_cursor_column(0) Reline::IOGate.erase_after_cursor - elsif not rendered - unless @in_pasting + else + if not rendered and not @in_pasting line = modify_lines(whole_lines)[@line_index] prompt, prompt_width, prompt_list = check_multiline_prompt(whole_lines, prompt) render_partial(prompt, prompt_width, line, @first_line_started_from) end + render_dialog((prompt_width + @cursor) % @screen_size.last) end @buffer_of_lines[@line_index] = @line @rest_height = 0 if @scroll_partial_screen @@ -472,6 +483,227 @@ class Reline::LineEditor https://github.com/ruby/ruby/blob/trunk/lib/reline/line_editor.rb#L483 end end + class DialogProcScope + def initialize(line_editor, proc_to_exec) + @line_editor = line_editor + @proc_to_exec = proc_to_exec + @cursor_pos = Reline::CursorPos.new + end + + def retrieve_completion_block(set_completion_quote_character = false) + @line_editor.retrieve_completion_block(set_completion_quote_character) + end + + def call_completion_proc_with_checking_args(pre, target, post) + @line_editor.call_completion_proc_with_checking_args(pre, target, post) + end + + def set_cursor_pos(col, row) + @cursor_pos.x = col + @cursor_pos.y = row + end + + def cursor_pos + @cursor_pos + end + + def just_cursor_moving + @line_editor.instance_variable_get(:@just_cursor_moving) + end + + def call + instance_exec(&@proc_to_exec) + end + end + + def dialog_proc=(p) + @dialog_proc_scope = DialogProcScope.new(self, p) + @dialog_proc = p + end + + DIALOG_HEIGHT = 5 + DIALOG_WIDTH = 40 + private def render_dialog(cursor_column) + return if @dialog_proc_scope.nil? + if @in_pasting + @dialog_contents = nil + return + end + @dialog_proc_scope.set_cursor_pos(cursor_column, @first_line_started_from + @started_from) + pos, result = @dialog_proc_scope.call + old_dialog_contents = @dialog_contents + old_dialog_contents_width = @dialog_contents_width + old_dialog_column = @dialog_column + old_dialog_vertical_offset = @dialog_vertical_offset + old_dialog_updown = @dialog_updown + if result and not result.empty? + @dialog_contents = result + @dialog_contents_width = @dialog_contents.map{ |c| calculate_width(c) } + else + clear_dialog + @dialog_contents = nil + return + end + upper_space = @first_line_started_from - @started_from + lower_space = @highest_in_all - @first_line_started_from - @started_from - 1 + @dialog_updown = nil + @dialog_column = pos.x + if (lower_space + @rest_height) >= DIALOG_HEIGHT + @dialog_updown = :down + @dialog_vertical_offset = 1 + elsif upper_space >= DIALOG_HEIGHT + @dialog_updown = :up + @dialog_vertical_offset = -(DIALOG_HEIGHT + 1) + else + if (lower_space + @rest_height) < DIALOG_HEIGHT + down = DIALOG_HEIGHT - (lower_space + @rest_height) + scroll_down(down) + move_cursor_up(DIALOG_HEIGHT - 1) + end + @dialog_updown = :down + @dialog_vertical_offset = 1 + end + reset_dialog(old_dialog_contents, old_dialog_contents_width, old_dialog_column, old_dialog_vertical_offset, old_dialog_updown) + ue = ?/ + ?~ * (DIALOG_WIDTH - 2) + ?\\ + sita = ?\\ + ?_ * (DIALOG_WIDTH - 2) + ?/ + yoko = ?: + case @dialog_updown + when :down + move_cursor_down(1) + when :up + end + Reline::IOGate.move_cursor_column(@dialog_column) + @output.write ue + Reline::IOGate.move_cursor_column(@dialog_column) + @dialog_contents = @dialog_contents[0...(DIALOG_HEIGHT - 2)] if @dialog_contents.size > (DIALOG_HEIGHT - 2) + @dialog_contents.each do |item| + move_cursor_down(1) + @output.write yoko + @output.write "%-#{DIALOG_WIDTH - 2}s" % item.slice(0, DIALOG_WIDTH - 2) + @output.write yoko + Reline::IOGate.move_cursor_column(@dialog_column) + end + move_cursor_down(1) + @output.write sita + Reline::IOGate.move_cursor_column(cursor_column) + case @dialog_updown + when :down + move_cursor_up(@dialog_contents.size + 2) + when :up + end + @dialog_lines_backup = { + lines: modify_lines(whole_lines), + line_index: @line_index, + started_from: @started_from, + byte_pointer: @byte_pointer + } + end + + private def reset_dialog(old_dialog_contents, old_dialog_contents_width, old_dialog_column, old_dialog_vertical_offset, old_dialog_updown) + return if @dialog_lines_backup.nil? or old_dialog_contents.nil? + prompt, prompt_width, prompt_list = check_multiline_prompt(@dialog_lines_backup[:lines], prompt) + visual_lines = [] + visual_start = nil + @dialog_lines_backup[:lines].each_with_index { |l, i| + pr = prompt_list ? prompt_list[i] : prompt + vl, height = split_by_width(pr + l, @screen_size.last) + if i == @dialog_lines_backup[:line_index] + visual_start = visual_lines.size + @dialog_lines_backup[:started_from] + end + visual_lines.concat(vl) + } + if old_dialog_vertical_offset < @dialog_vertical_offset + move_cursor_down(old_dialog_vertical_offset) + start = visual_start + old_dialog_vertical_offset + line_num = @dialog_vertical_offset - old_dialog_vertical_offset + line_num.times do |i| + Reline::IOGate.move_cursor_column(0) + @output.write visual_lines[sta (... truncated) -- ML: ruby-changes@q... Info: http://www.atdot.net/~ko1/quickml/