katakata_irb 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +23 -0
- data/LICENSE.txt +21 -0
- data/README.md +51 -0
- data/Rakefile +12 -0
- data/bin/console +10 -0
- data/bin/setup +8 -0
- data/exe/kirb +10 -0
- data/katakata_irb.gemspec +36 -0
- data/lib/katakata_irb/completor.rb +187 -0
- data/lib/katakata_irb/reline_patch.rb +40 -0
- data/lib/katakata_irb/reline_patches/escapeseq.patch +45 -0
- data/lib/katakata_irb/reline_patches/fullwidth.patch +200 -0
- data/lib/katakata_irb/reline_patches/indent.patch +25 -0
- data/lib/katakata_irb/reline_patches/raw.patch +95 -0
- data/lib/katakata_irb/reline_patches/scrollbar.patch +43 -0
- data/lib/katakata_irb/reline_patches/wholelines.patch +102 -0
- data/lib/katakata_irb/ruby_lex_patch.rb +197 -0
- data/lib/katakata_irb/trex.rb +207 -0
- data/lib/katakata_irb/type_simulator.rb +1108 -0
- data/lib/katakata_irb/types.rb +341 -0
- data/lib/katakata_irb/version.rb +5 -0
- data/lib/katakata_irb.rb +15 -0
- data/sig/katakata_irb.rbs +4 -0
- metadata +84 -0
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
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
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
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)
|