katakata_irb 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 07b0eb3288cc4eec15c83de9bd27948379bca2c1dfa233104cba59dff6826c7c
4
+ data.tar.gz: 20d4feb90c4df7e10554e045addfb242af50205dd0b187635a81ac3bc5b073b2
5
+ SHA512:
6
+ metadata.gz: 7a376d7deb729f3a2e2f3b18041e89606d668e331ece73c32d5b7ae9ff6b8d87bffa89f4ab3d1dc66c8354b3af2ad733bee211868e01bd8814055f3cc72fe83b
7
+ data.tar.gz: 50eaa7061da8609623f1fccc5d0fbe12685f888d7a565eede06e19993b435aa766ce6458668fa787369bef62ce5601b6dd9c4118fa534457c89341881367722d
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in katakata_irb.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "minitest", "~> 5.0"
data/Gemfile.lock ADDED
@@ -0,0 +1,23 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ katakata_irb (0.1.0)
5
+ rbs
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ minitest (5.16.3)
11
+ rake (13.0.6)
12
+ rbs (2.7.0)
13
+
14
+ PLATFORMS
15
+ x86_64-darwin-20
16
+
17
+ DEPENDENCIES
18
+ katakata_irb!
19
+ minitest (~> 5.0)
20
+ rake (~> 13.0)
21
+
22
+ BUNDLED WITH
23
+ 2.4.0.dev
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 tompng
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # KatakataIrb: IRB with Kata(型 Type) completion
2
+
3
+ KatakataIrb might provide a better autocompletion based on type analysis to irb.
4
+
5
+ ## Installation
6
+
7
+ ```
8
+ gem install katakata_irb
9
+ ```
10
+ ## Usage
11
+
12
+ ```
13
+ % kirb
14
+ irb(main):001:0> [1,'a'].sample.a█
15
+ |[1,'a'].sample.abs |
16
+ |[1,'a'].sample.abs2 |
17
+ |[1,'a'].sample.allbits? |
18
+ |[1,'a'].sample.angle |
19
+ |[1,'a'].sample.anybits? |
20
+ |[1,'a'].sample.arg |
21
+ |[1,'a'].sample.ascii_only?|
22
+ ```
23
+
24
+ ```
25
+ % kirb
26
+ irb(main):001:0> a = 10
27
+ => 10
28
+ irb(main):002:1* if true
29
+ irb(main):003:2* b = a.times.map do
30
+ irb(main):004:2* _1.to_s
31
+ irb(main):005:1* end
32
+ irb(main):006:1* b[0].a█
33
+ |b[0].ascii_only?|
34
+ ```
35
+
36
+ ```ruby
37
+ require 'katakata_irb/completor'
38
+ KatakataIrb::Completor.setup
39
+ 10.times do |i|
40
+ binding.irb
41
+ end
42
+ ```
43
+
44
+ ## Options
45
+
46
+ ### `kirb --debug-output`
47
+ Show debug output if it meets unimplemented syntax or something
48
+
49
+ ### `kirb --without-patch`
50
+ `kirb` will apply some patches to reline and irb/ruby-lex.rb by default. This option will disable it.
51
+ See `lib/katakata_irb/ruby_lex_patch.rb` and `lib/katakata_irb/reline_patches/*.patch`
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/test_*.rb"]
10
+ end
11
+
12
+ task default: :test
data/bin/console ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative '../lib/katakata_irb'
3
+ require_relative '../lib/katakata_irb/reline_patch'
4
+ KatakataIrb::RelinePatch.require_patched_reline
5
+ KatakataIrb.log_output = STDERR
6
+ require 'bundler/setup'
7
+ require 'katakata_irb'
8
+ require 'katakata_irb/ruby_lex_patch'
9
+ KatakataIrb::RubyLexPatch.patch_to_ruby_lex
10
+ KatakataIrb.repl
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/exe/kirb ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ unless ARGV.delete '--without-patch'
3
+ require_relative '../lib/katakata_irb/reline_patch'
4
+ KatakataIrb::RelinePatch.require_patched_reline
5
+ require 'katakata_irb/ruby_lex_patch'
6
+ KatakataIrb::RubyLexPatch.patch_to_ruby_lex
7
+ end
8
+ require 'katakata_irb'
9
+ KatakataIrb.log_output = STDERR if ARGV.delete '--debug-output'
10
+ KatakataIrb.repl
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/katakata_irb/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "katakata_irb"
7
+ spec.version = KatakataIrb::VERSION
8
+ spec.authors = ["tompng"]
9
+ spec.email = ["tomoyapenguin@gmail.com"]
10
+
11
+ spec.summary = "IRB with Typed Completion"
12
+ spec.description = "IRB with Typed Completion"
13
+ spec.homepage = "http://github.com/tompng/katakata_irb"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 3.1.0"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = "http://github.com/tompng/katakata_irb"
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
23
+ `git ls-files -z`.split("\x0").reject do |f|
24
+ (f == __FILE__) || f.match(%r{\A(?:(?:test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
25
+ end
26
+ end
27
+ spec.bindir = "exe"
28
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ["lib"]
30
+
31
+ # Uncomment to register a new dependency of your gem
32
+ spec.add_dependency 'rbs'
33
+
34
+ # For more information and examples about making a new gem, check out our
35
+ # guide at: https://bundler.io/guides/creating_gem.html
36
+ end
@@ -0,0 +1,187 @@
1
+ require_relative 'trex'
2
+ require_relative 'type_simulator'
3
+ require 'rbs'
4
+ require 'rbs/cli'
5
+ require 'irb'
6
+
7
+ module KatakataIrb::Completor
8
+ using KatakataIrb::TypeSimulator::LexerElemMatcher
9
+
10
+ HIDDEN_METHODS = %w[Namespace TypeName] # defined by rbs, should be hidden
11
+
12
+ def self.setup
13
+ completion_proc = ->(target, preposing = nil, postposing = nil) do
14
+ code = "#{preposing}#{target}"
15
+ irb_context = IRB.conf[:MAIN_CONTEXT]
16
+ binding = irb_context.workspace.binding
17
+ candidates = case analyze code, binding
18
+ in [:require | :require_relative => method, name]
19
+ if method == :require
20
+ IRB::InputCompletor.retrieve_files_to_require_from_load_path
21
+ else
22
+ IRB::InputCompletor.retrieve_files_to_require_relative_from_current_dir
23
+ end
24
+ in [:call_or_const, type, name, self_call]
25
+ ((self_call ? type.all_methods: type.methods).map(&:to_s) - HIDDEN_METHODS) | type.constants
26
+ in [:const, type, name]
27
+ type.constants
28
+ in [:ivar, name, _scope]
29
+ # TODO: scope
30
+ ivars = binding.eval('self').instance_variables rescue []
31
+ cvars = (binding.eval('self').class_variables rescue nil) if name == '@'
32
+ ivars | (cvars || [])
33
+ in [:cvar, name, _scope]
34
+ # TODO: scope
35
+ binding.eval('self').class_variables rescue []
36
+ in [:gvar, name]
37
+ global_variables
38
+ in [:symbol, name]
39
+ Symbol.all_symbols
40
+ in [:call, type, name, self_call]
41
+ (self_call ? type.all_methods : type.methods).map(&:to_s) - HIDDEN_METHODS
42
+ in [:lvar_or_method, name, scope]
43
+ scope.self_type.all_methods.map(&:to_s) | scope.local_variables
44
+ else
45
+ []
46
+ end
47
+ all_symbols_pattern = /\A[ -\/:-@\[-`\{-~]*\z/
48
+ candidates.map(&:to_s).select { !_1.match?(all_symbols_pattern) && _1.start_with?(name) }.uniq.sort.map do
49
+ target + _1[name.size..]
50
+ end
51
+ end
52
+ IRB::InputCompletor::CompletionProc.define_singleton_method :call do |*args|
53
+ completion_proc.call(*args)
54
+ end
55
+ end
56
+
57
+ def self.analyze(code, binding = Kernel.binding)
58
+ lvars_code = binding.local_variables.map do |name|
59
+ "#{name}="
60
+ end.join + "nil;\n"
61
+ code = lvars_code + code
62
+ tokens = RubyLex.ripper_lex_without_warning code
63
+ tokens = KatakataIrb::TRex.interpolate_ripper_ignored_tokens code, tokens
64
+ last_opens, unclosed_heredocs = KatakataIrb::TRex.parse(tokens)
65
+ closings = (last_opens + unclosed_heredocs).map do |t,|
66
+ case t.tok
67
+ when /\A%.[<>]\z/
68
+ '>'
69
+ when '{', '#{', /\A%.?[{}]\z/
70
+ '}'
71
+ when '(', /\A%.?[()]\z/
72
+ ')'
73
+ when '[', /\A%.?[\[\]]\z/
74
+ ']'
75
+ when /\A%.?(.)\z/
76
+ $1
77
+ when '"', "'", '/', '`'
78
+ t.tok
79
+ when /\A<<(?:"(?<s>.+)"|'(?<s>.+)'|(?<s>.+))/
80
+ $3
81
+ else
82
+ 'end'
83
+ end
84
+ end
85
+
86
+ return if code =~ /[!?]\z/
87
+ case tokens.last
88
+ in { event: :on_ignored_by_ripper, tok: '.' }
89
+ suffix = 'method'
90
+ name = ''
91
+ in { dot: true }
92
+ suffix = 'method'
93
+ name = ''
94
+ in { event: :on_ident | :on_kw, tok: }
95
+ return unless code.delete_suffix! tok
96
+ suffix = 'method'
97
+ name = tok
98
+ in { event: :on_const, tok: }
99
+ return unless code.delete_suffix! tok
100
+ suffix = 'Const'
101
+ name = tok
102
+ in { event: :on_tstring_content, tok: }
103
+ return unless code.delete_suffix! tok
104
+ suffix = 'string'
105
+ name = tok.rstrip
106
+ else
107
+ return
108
+ end
109
+
110
+ closings = $/ + closings.reverse.join($/)
111
+ sexp = Ripper.sexp code + suffix + closings
112
+ lines = code.lines
113
+ line_no = lines.size
114
+ col = lines.last.bytesize
115
+ if lines.last.end_with? "\n"
116
+ line_no += 1
117
+ col = 0
118
+ end
119
+
120
+ if sexp in [:program, [lvars_exp, *rest_statements]]
121
+ sexp = [:program, rest_statements]
122
+ end
123
+
124
+ *parents, expression, target = find_target sexp, line_no, col
125
+ in_class_module = parents&.any? { _1 in [:class | :module,] }
126
+ icvar_available = !in_class_module
127
+ return unless target in [type, String, [Integer, Integer]]
128
+ if target in [:@ivar,]
129
+ return [:ivar, name] if icvar_available
130
+ elsif target in [:@cvar,]
131
+ return [:cvar, name] if icvar_available
132
+ end
133
+ return unless expression
134
+ if (target in [:@tstring_content,]) && (parents[-4] in [:command, [:@ident, 'require' | 'require_relative' => require_method,],])
135
+ return [require_method.to_sym, name.rstrip]
136
+ end
137
+ calculate_scope = -> { KatakataIrb::TypeSimulator.calculate_binding_scope binding, parents, expression }
138
+ calculate_receiver = -> receiver { KatakataIrb::TypeSimulator.calculate_receiver binding, parents, receiver }
139
+ case expression
140
+ in [:vcall | :var_ref, [:@ident,]]
141
+ [:lvar_or_method, name, calculate_scope.call]
142
+ in [:symbol, [:@ident | :@const | :@op | :@kw,]]
143
+ [:symbol, name]
144
+ in [:var_ref | :const_ref, [:@const,]]
145
+ # TODO
146
+ [:const, KatakataIrb::Types::SingletonType.new(Object), name]
147
+ in [:var_ref, [:@gvar,]]
148
+ [:gvar, name]
149
+ in [:var_ref, [:@ivar,]]
150
+ [:ivar, name, calculate_scope.call.self_type] if icvar_available
151
+ in [:var_ref, [:@cvar,]]
152
+ [:cvar, name, calculate_scope.call.self_type] if icvar_available
153
+ in [:call, receiver, [:@period,] | [:@op, '&.',] | :'::' => dot, [:@ident | :@const,]]
154
+ self_call = (receiver in [:var_ref, [:@kw, 'self',]])
155
+ [dot == :'::' ? :call_or_const : :call, calculate_receiver.call(receiver), name, self_call]
156
+ in [:const_path_ref, receiver, [:@const,]]
157
+ [:const, calculate_receiver.call(receiver), name]
158
+ in [:top_const_ref, [:@const,]]
159
+ [:const, KatakataIrb::Types::SingletonType.new(Object), name]
160
+ in [:def,] | [:string_content,] | [:var_field,] | [:defs,] | [:rest_param,] | [:kwrest_param,] | [:blockarg,] | [[:@ident,],]
161
+ in [Array,] # `xstring`, /regexp/
162
+ else
163
+ KatakataIrb.log_puts
164
+ KatakataIrb.log_puts [:NEW_EXPRESSION, expression].inspect
165
+ KatakataIrb.log_puts
166
+ end
167
+ end
168
+
169
+ def self.find_target(sexp, line, col, stack = [sexp])
170
+ return unless sexp.is_a? Array
171
+ sexp.each do |child|
172
+ case child
173
+ in [Symbol, String, [Integer => l, Integer => c]]
174
+ if l == line && c == col
175
+ stack << child
176
+ return stack
177
+ end
178
+ else
179
+ stack << child
180
+ result = find_target(child, line, col, stack)
181
+ return result if result
182
+ stack.pop
183
+ end
184
+ end
185
+ nil
186
+ end
187
+ end
@@ -0,0 +1,40 @@
1
+ module KatakataIrb; end
2
+ module KatakataIrb::RelinePatch
3
+ module RelinePatchIseqLoader; end
4
+ def self.require_patched_reline
5
+ # Apply patches of unmerged pull-request to reline
6
+ patches = %w[wholelines escapeseq indent fullwidth raw scrollbar]
7
+ patched = {}
8
+ require 'reline/version.rb' # result of $LOAD_PATH.resolve_feature_path will change after this require
9
+ patches.each do |patch_name|
10
+ patch = File.read File.expand_path("reline_patches/#{patch_name}.patch", File.dirname(__FILE__))
11
+ current_patched = {}
12
+ patch.gsub(/^diff.+\nindex.+$/, '').split(/^--- a(.+)\n\+\+\+ b(.+)\n/).drop(1).each_slice(3) do |file, newfile, diff|
13
+ raise if file != newfile
14
+ _, path = $LOAD_PATH.resolve_feature_path file.sub(%r{^/lib/}, '')
15
+ code = current_patched[path] || patched[path] || File.read(path)
16
+ diff.split(/^@@.+\n/).drop(1).map(&:lines).each do |lines|
17
+ target = lines.reject { _1[0] == '+' }.map { _1[1..] }.join
18
+ replace = lines.reject { _1[0] == '-' }.map { _1[1..] }.join
19
+ raise unless code.include? target
20
+ code.sub! target, replace
21
+ end
22
+ current_patched[path] = code
23
+ end
24
+ patched.update current_patched
25
+ rescue
26
+ puts "Failed to apply katakata_irb/reline_patches/#{patch_name}.patch to reline"
27
+ end
28
+
29
+ RelinePatchIseqLoader.define_method :load_iseq do |fname|
30
+ if patched.key? fname
31
+ RubyVM::InstructionSequence.compile patched[fname], fname
32
+ else
33
+ RubyVM::InstructionSequence.compile_file fname
34
+ end
35
+ end
36
+ RubyVM::InstructionSequence.singleton_class.prepend RelinePatchIseqLoader
37
+ require 'reline'
38
+ RelinePatchIseqLoader.undef_method :load_iseq
39
+ end
40
+ end
@@ -0,0 +1,45 @@
1
+ diff --git a/lib/reline/line_editor.rb b/lib/reline/line_editor.rb
2
+ index 1c33a4b..bbf5e6c 100644
3
+ --- a/lib/reline/line_editor.rb
4
+ +++ b/lib/reline/line_editor.rb
5
+ @@ -663,8 +663,10 @@ class Reline::LineEditor
6
+ dialog.set_cursor_pos(cursor_column, @first_line_started_from + @started_from)
7
+ dialog_render_info = dialog.call(@last_key)
8
+ if dialog_render_info.nil? or dialog_render_info.contents.nil? or dialog_render_info.contents.empty?
9
+ + lines = whole_lines
10
+ dialog.lines_backup = {
11
+ - lines: modify_lines(whole_lines),
12
+ + unmodified_lines: lines,
13
+ + lines: modify_lines(lines),
14
+ line_index: @line_index,
15
+ first_line_started_from: @first_line_started_from,
16
+ started_from: @started_from,
17
+ @@ -766,8 +768,10 @@ class Reline::LineEditor
18
+ Reline::IOGate.move_cursor_column(cursor_column)
19
+ move_cursor_up(dialog.vertical_offset + dialog.contents.size - 1)
20
+ Reline::IOGate.show_cursor
21
+ + lines = whole_lines
22
+ dialog.lines_backup = {
23
+ - lines: modify_lines(whole_lines),
24
+ + unmodified_lines: lines,
25
+ + lines: modify_lines(lines),
26
+ line_index: @line_index,
27
+ first_line_started_from: @first_line_started_from,
28
+ started_from: @started_from,
29
+ @@ -777,7 +781,7 @@ class Reline::LineEditor
30
+ private def reset_dialog(dialog, old_dialog)
31
+ return if dialog.lines_backup.nil? or old_dialog.contents.nil?
32
+ - prompt, prompt_width, prompt_list = check_multiline_prompt(dialog.lines_backup[:lines])
33
+ + prompt, prompt_width, prompt_list = check_multiline_prompt(dialog.lines_backup[:unmodified_lines])
34
+ visual_lines = []
35
+ visual_start = nil
36
+ dialog.lines_backup[:lines].each_with_index { |l, i|
37
+ @@ -888,7 +892,7 @@ class Reline::LineEditor
38
+ private def clear_each_dialog(dialog)
39
+ dialog.trap_key = nil
40
+ return unless dialog.contents
41
+ - prompt, prompt_width, prompt_list = check_multiline_prompt(dialog.lines_backup[:lines])
42
+ + prompt, prompt_width, prompt_list = check_multiline_prompt(dialog.lines_backup[:unmodified_lines])
43
+ visual_lines = []
44
+ visual_lines_under_dialog = []
45
+ visual_start = nil
@@ -0,0 +1,200 @@
1
+ diff --git a/lib/reline/line_editor.rb b/lib/reline/line_editor.rb
2
+ index c9e613e..6acf969 100644
3
+ --- a/lib/reline/line_editor.rb
4
+ +++ b/lib/reline/line_editor.rb
5
+ @@ -748,3 +741,3 @@ class Reline::LineEditor
6
+ end
7
+ str_width = dialog.width - (dialog.scrollbar_pos.nil? ? 0 : @block_elem_width)
8
+ - str = padding_space_with_escape_sequences(Reline::Unicode.take_range(item, 0, str_width), str_width)
9
+ + str, = Reline::Unicode.take_range(item, 0, str_width, padding: true)
10
+ @@ -793,85 +786,42 @@ class Reline::LineEditor
11
+ end
12
+ visual_lines.concat(vl)
13
+ }
14
+ - old_y = dialog.lines_backup[:first_line_started_from] + dialog.lines_backup[:started_from]
15
+ - y = @first_line_started_from + @started_from
16
+ - y_diff = y - old_y
17
+ - if (old_y + old_dialog.vertical_offset) < (y + dialog.vertical_offset)
18
+ - # rerender top
19
+ - move_cursor_down(old_dialog.vertical_offset - y_diff)
20
+ - start = visual_start + old_dialog.vertical_offset
21
+ - line_num = dialog.vertical_offset - old_dialog.vertical_offset
22
+ - line_num.times do |i|
23
+ - Reline::IOGate.move_cursor_column(old_dialog.column)
24
+ - if visual_lines[start + i].nil?
25
+ - s = ' ' * old_dialog.width
26
+ - else
27
+ - s = Reline::Unicode.take_range(visual_lines[start + i], old_dialog.column, old_dialog.width)
28
+ - s = padding_space_with_escape_sequences(s, old_dialog.width)
29
+ - end
30
+ - @output.write "\e[0m#{s}\e[0m"
31
+ - move_cursor_down(1) if i < (line_num - 1)
32
+ - end
33
+ - move_cursor_up(old_dialog.vertical_offset + line_num - 1 - y_diff)
34
+ - end
35
+ - if (old_y + old_dialog.vertical_offset + old_dialog.contents.size) > (y + dialog.vertical_offset + dialog.contents.size)
36
+ - # rerender bottom
37
+ - move_cursor_down(dialog.vertical_offset + dialog.contents.size - y_diff)
38
+ - start = visual_start + dialog.vertical_offset + dialog.contents.size
39
+ - line_num = (old_dialog.vertical_offset + old_dialog.contents.size) - (dialog.vertical_offset + dialog.contents.size)
40
+ - line_num.times do |i|
41
+ - Reline::IOGate.move_cursor_column(old_dialog.column)
42
+ - if visual_lines[start + i].nil?
43
+ - s = ' ' * old_dialog.width
44
+ - else
45
+ - s = Reline::Unicode.take_range(visual_lines[start + i], old_dialog.column, old_dialog.width)
46
+ - s = padding_space_with_escape_sequences(s, old_dialog.width)
47
+ + old_dialog_y = dialog.lines_backup[:first_line_started_from] + dialog.lines_backup[:started_from]
48
+ + dialog_y = @first_line_started_from + @started_from
49
+ +
50
+ + x_range = dialog.column...dialog.column + dialog.width
51
+ + old_x_range = old_dialog.column...old_dialog.column + old_dialog.width
52
+ + y_range = dialog_y + dialog.vertical_offset...dialog_y + dialog.vertical_offset + dialog.contents.size
53
+ + old_y_range = old_dialog_y + old_dialog.vertical_offset...old_dialog_y + old_dialog.vertical_offset + old_dialog.contents.size
54
+ +
55
+ + cursor_y = dialog_y
56
+ + old_y_range.each do |y|
57
+ + rerender_ranges = []
58
+ + if y_range.cover?(y) && x_range.any?(old_x_range)
59
+ + if old_x_range.begin < x_range.begin
60
+ + # rerender left
61
+ + rerender_ranges << [old_x_range.begin...[old_x_range.end, x_range.begin].min, true, false]
62
+ end
63
+ - @output.write "\e[0m#{s}\e[0m"
64
+ - move_cursor_down(1) if i < (line_num - 1)
65
+ - end
66
+ - move_cursor_up(dialog.vertical_offset + dialog.contents.size + line_num - 1 - y_diff)
67
+ - end
68
+ - if old_dialog.column < dialog.column
69
+ - # rerender left
70
+ - move_cursor_down(old_dialog.vertical_offset - y_diff)
71
+ - width = dialog.column - old_dialog.column
72
+ - start = visual_start + old_dialog.vertical_offset
73
+ - line_num = old_dialog.contents.size
74
+ - line_num.times do |i|
75
+ - Reline::IOGate.move_cursor_column(old_dialog.column)
76
+ - if visual_lines[start + i].nil?
77
+ - s = ' ' * width
78
+ - else
79
+ - s = Reline::Unicode.take_range(visual_lines[start + i], old_dialog.column, width)
80
+ - s = padding_space_with_escape_sequences(s, dialog.width)
81
+ + if x_range.end < old_x_range.end
82
+ + # rerender right
83
+ + rerender_ranges << [[x_range.end, old_x_range.begin].max...old_x_range.end, false, true]
84
+ end
85
+ + else
86
+ + rerender_ranges << [old_x_range, true, true]
87
+ + end
88
+ +
89
+ + rerender_ranges.each do |range, cover_begin, cover_end|
90
+ + move_cursor_down(y - cursor_y)
91
+ + cursor_y = y
92
+ + col = range.begin
93
+ + width = range.end - range.begin
94
+ + line = visual_lines[y + visual_start - old_dialog_y] || ''
95
+ + s, col = Reline::Unicode.take_range(line, col, width, cover_begin: cover_begin, cover_end: cover_end, padding: true)
96
+ + Reline::IOGate.move_cursor_column(col)
97
+ @output.write "\e[0m#{s}\e[0m"
98
+ - move_cursor_down(1) if i < (line_num - 1)
99
+ - end
100
+ - move_cursor_up(old_dialog.vertical_offset + line_num - 1 - y_diff)
101
+ - end
102
+ - if (old_dialog.column + old_dialog.width) > (dialog.column + dialog.width)
103
+ - # rerender right
104
+ - move_cursor_down(old_dialog.vertical_offset + y_diff)
105
+ - width = (old_dialog.column + old_dialog.width) - (dialog.column + dialog.width)
106
+ - start = visual_start + old_dialog.vertical_offset
107
+ - line_num = old_dialog.contents.size
108
+ - line_num.times do |i|
109
+ - Reline::IOGate.move_cursor_column(old_dialog.column + dialog.width)
110
+ - if visual_lines[start + i].nil?
111
+ - s = ' ' * width
112
+ - else
113
+ - s = Reline::Unicode.take_range(visual_lines[start + i], old_dialog.column + dialog.width, width)
114
+ - rerender_width = old_dialog.width - dialog.width
115
+ - s = padding_space_with_escape_sequences(s, rerender_width)
116
+ - end
117
+ - Reline::IOGate.move_cursor_column(dialog.column + dialog.width)
118
+ - @output.write "\e[0m#{s}\e[0m"
119
+ - move_cursor_down(1) if i < (line_num - 1)
120
+ end
121
+ - move_cursor_up(old_dialog.vertical_offset + line_num - 1 + y_diff)
122
+ end
123
+ + move_cursor_up(cursor_y - dialog_y)
124
+ Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
125
+ end
126
+
127
+ @@ -912,9 +862,8 @@ class Reline::LineEditor
128
+ dialog_vertical_size = dialog.contents.size
129
+ dialog_vertical_size.times do |i|
130
+ if i < visual_lines_under_dialog.size
131
+ - Reline::IOGate.move_cursor_column(dialog.column)
132
+ - str = Reline::Unicode.take_range(visual_lines_under_dialog[i], dialog.column, dialog.width)
133
+ - str = padding_space_with_escape_sequences(str, dialog.width)
134
+ + str, start_pos = Reline::Unicode.take_range(visual_lines_under_dialog[i], dialog.column, dialog.width, cover_begin: true, cover_end: true, padding: true)
135
+ + Reline::IOGate.move_cursor_column(start_pos)
136
+ @output.write "\e[0m#{str}\e[0m"
137
+ else
138
+ Reline::IOGate.move_cursor_column(dialog.column)
139
+ diff --git a/lib/reline/unicode.rb b/lib/reline/unicode.rb
140
+ index 6000c9f..03074dd 100644
141
+ --- a/lib/reline/unicode.rb
142
+ +++ b/lib/reline/unicode.rb
143
+ @@ -194,17 +194,21 @@ class Reline::Unicode
144
+ end
145
+
146
+ # Take a chunk of a String cut by width with escape sequences.
147
+ - def self.take_range(str, start_col, max_width, encoding = str.encoding)
148
+ + def self.take_range(str, start_col, width, encoding: str.encoding, cover_begin: false, cover_end: false, padding: false)
149
+ chunk = String.new(encoding: encoding)
150
+ total_width = 0
151
+ rest = str.encode(Encoding::UTF_8)
152
+ in_zero_width = false
153
+ + chunk_start_col = nil
154
+ + chunk_end_col = nil
155
+ rest.scan(WIDTH_SCANNER) do |gc|
156
+ case
157
+ when gc[NON_PRINTING_START_INDEX]
158
+ in_zero_width = true
159
+ + chunk << NON_PRINTING_START
160
+ when gc[NON_PRINTING_END_INDEX]
161
+ in_zero_width = false
162
+ + chunk << NON_PRINTING_END
163
+ when gc[CSI_REGEXP_INDEX]
164
+ chunk << gc[CSI_REGEXP_INDEX]
165
+ when gc[OSC_REGEXP_INDEX]
166
+ @@ -215,13 +219,31 @@ class Reline::Unicode
167
+ chunk << gc
168
+ else
169
+ mbchar_width = get_mbchar_width(gc)
170
+ + prev_width = total_width
171
+ total_width += mbchar_width
172
+ - break if (start_col + max_width) < total_width
173
+ - chunk << gc if start_col < total_width
174
+ + break if !cover_end && total_width > start_col + width
175
+ + if cover_begin ? start_col < total_width : start_col <= prev_width
176
+ + chunk << gc
177
+ + chunk_start_col ||= prev_width
178
+ + chunk_end_col = total_width
179
+ + end
180
+ + break if total_width >= start_col + width
181
+ end
182
+ end
183
+ end
184
+ - chunk
185
+ + chunk_start_col ||= start_col
186
+ + chunk_end_col ||= start_col
187
+ + if padding
188
+ + if start_col < chunk_start_col
189
+ + chunk = ' ' * (chunk_start_col - start_col) + chunk
190
+ + chunk_start_col = start_col
191
+ + end
192
+ + if chunk_end_col < start_col + width
193
+ + chunk << ' ' * (start_col + width - chunk_end_col)
194
+ + chunk_end_col = start_col + width
195
+ + end
196
+ + end
197
+ + [chunk, chunk_start_col, chunk_end_col - chunk_start_col]
198
+ end
199
+
200
+ def self.get_next_mbchar_size(line, byte_pointer)