katakata_irb 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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)