Last active
February 24, 2024 17:31
-
-
Save tompng/f70dda073b8044112c7d1cc8ed6b4d53 to your computer and use it in GitHub Desktop.
reline line_editor readme svg
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'irb' | |
require 'tempfile' | |
short_code = <<~RUBY | |
def foobar | |
puts 'hello' + 'world' + 'ruby' + 'happy' + 'hacking' | |
end | |
RUBY | |
long_code = <<~RUBY | |
class A | |
def foo | |
a = 'helloworldhelloworldhelloworldhelloworld' | |
puts a * 10 | |
end | |
def bar | |
puts 'ruby'+'ruby'+'ruby'+'ruby'+'ruby'+'ruby'+'ruby'+'ruby'+'ruby'+'ruby'+'ruby'+'ruby'+'ruby' | |
end | |
def baz | |
puts 'ruby' | |
end | |
end | |
RUBY | |
def escape_text(text) | |
text.gsub('<', '<').gsub('>', '>') | |
end | |
def calculate_prompt(code, offset = 0) | |
Tempfile.open do |f| | |
ENV['IRBRC'] = f.path | |
IRB.setup 'irb' | |
irb = IRB::Irb.new | |
io = Object.new | |
irb.context.io = io | |
dynamic_prompt = nil | |
irb.instance_variable_set(:@line_no, offset + 1) | |
def io.auto_indent; end | |
def (irb.context).prompting?() = true | |
io.define_singleton_method(:dynamic_prompt) do |&block| | |
dynamic_prompt = block | |
end | |
irb.configure_io | |
dynamic_prompt.(code.lines) | |
end | |
end | |
def colorize_concat_prompt(code, offset = 0) | |
prompts = calculate_prompt(code, offset) | |
IRB::Color.colorize_code(code).lines.zip(prompts).map { |line, prompt| (prompt + line).chomp } | |
end | |
COLORS = { | |
30 => 'black', 31 => 'red', 32 => 'green', 33 => 'yellow', 34 => 'blue', 35 => 'magenta', 36 => 'cyan', 37 => 'silver' | |
} | |
def lines_to_svg(lines, id: nil, width: nil, size: 20) | |
seq = [] | |
colored_lines = lines.map do |line| | |
line = line.chomp | |
colored_chars = [] | |
line.split(/(\e\[[\d;]*m)/).each_with_index do |s, i| | |
if i % 2 == 0 | |
s.chars.each do |c| | |
colored_chars << [seq, c] | |
end | |
else | |
numbers = s.scan(/\d+/) | |
seq = [] if numbers.empty? | |
seq += numbers.map(&:to_i) | |
zero = seq.rindex(0) | |
seq = seq[zero + 1..] if zero | |
end | |
end | |
colored_chars | |
end | |
colored_lines = colored_lines.map { _1.each_slice(width).to_a }.flatten(1) if width | |
svg = [] | |
colored_lines.each_with_index do |line, y| | |
x = 0 | |
line.chunk { _2 == ' ' ? :space : _1 }.each do |col, colored_chars| | |
text = colored_chars.map(&:last).join | |
w = text.size | |
if col != :space | |
color = 'black' | |
col.each do |n| | |
color = COLORS[n] if COLORS[n] | |
end | |
svg << %(<text x="#{x * size / 2}" y="#{size * 0.75 + y * size}" fill="#{color}" textLength="#{size * w / 2}">#{escape_text(text)}</text>) | |
end | |
x += w | |
end | |
end | |
[ | |
%(<g #{%(id="#{id}" ) if id}font-size="14px">), | |
*svg.map{ " #{_1}" }, | |
%(</g>) | |
].join("\n") | |
end | |
short_irb_template = lines_to_svg(colorize_concat_prompt(short_code, 2), width: 40, id: :short_code) | |
long_irb_template = lines_to_svg(colorize_concat_prompt(long_code, 0), width: 40, id: :long_code) | |
short_irb_output_template = self.then do | |
evals = [['1 + 2', "=> \e[34m3\e[m"], ['puts :hello', 'hello', "=> \e[36mnil\e[m"]] | |
output = evals.flat_map.with_index do |(code, *output), idx| | |
colorize_concat_prompt(code, idx) + output | |
end | |
lines_to_svg(output, width: 40, id: :irb_output) | |
end | |
template = [short_irb_template, long_irb_template, short_irb_output_template].join("\n") | |
def line_with_text((x1, y1), (x2, y2), text:, color: 'black', font_size: 16, line_width: 2, edge: [4, 4], edge_left: edge, edge_right: edge) | |
len = Math.hypot(x2 - x1, y2 - y1) | |
angle = 180 * Math.atan2(y2 - y1, x2 - x1) / Math::PI | |
down = edge[0] + line_width / 2 | |
up = edge[1] + line_width / 2 | |
<<~SVG | |
<g transform="translate(#{x1} #{y1}) rotate(#{angle} 0 0)"> | |
<line x1="0" y1="#{-(edge_left[1] + line_width / 2)}" x2="0" y2="#{edge_left[0] + line_width / 2}" stroke="#{color}" stroke-width="#{line_width}"/> | |
<line x1="0" y1="0" x2="#{len}" y2="0" stroke="#{color}" stroke-width="#{line_width}"/> | |
<line x1="#{len}" y1="#{-(edge_right[1] + line_width / 2)}" x2="#{len}" y2="#{edge_right[0] + line_width / 2}" stroke="#{color}" stroke-width="#{line_width}"/> | |
<text x="#{len / 2}" y="-#{2 * line_width + font_size / 8}" fill="#{color}" font-size="#{font_size}px" text-anchor="middle">#{text}</text> | |
</g> | |
SVG | |
end | |
def cursor(x, y) | |
%(<rect x="#{10 * x}" y="#{20 * y}" width="10" height="20" fill="#666" />) | |
end | |
FONT = "ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', Courier, Monaco, monospace" | |
def memo_to_svg(memo) | |
memo.lines.map.with_index do |line, idx| | |
%(<text y="#{12 * idx}">#{escape_text(line)}</text>) | |
end.join("\n") | |
end | |
def short_irb_view(label: false, uncontrolable_highlight: false, controlable_highlight: true, code_opacity: 0.4, background: 'silver') | |
<<~SVG | |
<rect x="-4" y="-2" width="408" height="204" fill="#{background}" stroke="black" stroke-width="4"/> | |
<use href="#irb_output" x="0" y="0" opacity="#{[0.4, code_opacity].min}"/> | |
#{cursor(23, 7)} | |
<use href="#short_code" x="0" y="100" opacity="#{code_opacity}"/> | |
#{%(<rect x="-1" y="1" width="402" height="98" stroke-width="2" fill="none" stroke="#F88"/>) if uncontrolable_highlight} | |
#{%(<text x="400" y="12" text-anchor="end" fill="black" font-size="10px">Uncontrolable area</text>) if label && uncontrolable_highlight} | |
#{%(<rect x="-1" y="101" width="402" height="98" fill="none" stroke-width="2" stroke="#88F"/>) if controlable_highlight} | |
#{%(<text x="400" y="198" text-anchor="end" fill="black" font-size="10px">Controlable area</text>) if label && controlable_highlight} | |
SVG | |
end | |
def long_irb_view(label: false) | |
<<~SVG | |
<rect x="-2" y="-80" width="404" height="340" fill="silver"/> | |
#{cursor(26, 5)} | |
<use href="#long_code" x="0" y="-80" opacity="0.4"/> | |
<rect x="-4" y="-2" width="408" height="204" fill="none" stroke="black" stroke-width="4"/> | |
<rect x="-1" y="1" width="402" height="198" fill="none" stroke-width="2" stroke="#88F"/> | |
#{%(<text x="400" y="198" text-anchor="end" fill="black" font-size="10px">Controlable area</text>) if label} | |
SVG | |
end | |
def svg_header(width, height) | |
<<~SVG | |
<?xml version="1.0" encoding="utf-8"?> | |
<svg xmlns="http://www.w3.org/2000/svg" width="#{width}px" height="#{height}px" viewBox="0 0 #{width} #{height}"> | |
<rect x="0" y="0" width="#{width}" height="#{height}" fill="white"/> | |
SVG | |
end | |
def indent_svg(svg) | |
indent = 0 | |
lines = svg.gsub('><', ">\n<").split(/\n[ \n]*/).map do |s| | |
current_indent = indent | |
if s.start_with?('</') | |
current_indent -= 1 | |
indent -= 1 | |
elsif s.start_with?('<') && !s.start_with?('<?') && !s.match?(/<\/[^<>]+ *>$/) && !s.match?(/\/ *>/) | |
indent += 1 | |
end | |
' ' * (2 * current_indent) + s + "\n" | |
end | |
warn 'wrong indent' unless indent == 0 | |
lines.join | |
end | |
def create_short_long_file(file, template, content_left, content_right) | |
File.write file, indent_svg(<<~SVG) | |
#{svg_header(1000, 400)} | |
<defs> | |
#{template} | |
</defs> | |
<g font-family="#{FONT}"> | |
<g transform="translate(40 120)"> | |
#{short_irb_view} | |
#{content_left} | |
</g> | |
<g transform="translate(540 120)"> | |
#{long_irb_view} | |
#{content_right} | |
</g> | |
</g> | |
</svg> | |
SVG | |
end | |
create_short_long_file 'line_editor_screen.svg', template, <<~EOS, <<~EOS | |
<text x="200" y="-7" font-size="10px" text-anchor="middle">@scroll_partial_screen == 0</text> | |
#{line_with_text([235, 100], [235, 140], text: '', font_size: 10)} | |
<text x="238" y="125" font-size="10px">wrapped_cursor_position[1]</text> | |
#{line_with_text([0, 150], [230, 150], text: 'wrapped_cursor_position[0]', font_size: 10)} | |
EOS | |
#{line_with_text([-10, 0], [-10, -80], text: '@scroll_partial_screen', font_size: 10, edge: [2, 0])} | |
#{line_with_text([265, -80], [265, 100], text: 'wrapped_cursor_position[1]', font_size: 10)} | |
#{line_with_text([0, 110], [260, 110], text: 'wrapped_cursor_position[0]', font_size: 10)} | |
EOS | |
create_short_long_file 'line_editor_screen_legacy.svg', template, <<~EOS, <<~EOS | |
<text x="200" y="-7" font-size="10px" text-anchor="middle">@scroll_partial_screen == nil</text> | |
#{line_with_text([0, 140], [230, 140], text: '@cursor', font_size: 10, edge: [2, 0])} | |
#{line_with_text([0, 160], [300, 160], text: '@cursor_max', font_size: 10, edge: [0, 2])} | |
<line x1="0" y1="120" x2="400" y2="120" stroke-width="1" stroke="black" /> | |
<line x1="0" y1="160" x2="400" y2="160" stroke-width="1" stroke="black" /> | |
#{line_with_text([235, 160], [235, 200], text: '', font_size: 10, edge: [2, 0])} | |
<text x="238" y="182" font-size="10px">@rest_height</text> | |
#{line_with_text([410, 120], [410, 160], text: '@highest_in_this', font_size: 10, edge: [2, 0])} | |
#{line_with_text([-10, 180], [-10, 100], text: '@highest_in_all', font_size: 10, edge: [2, 0])} | |
#{line_with_text([235, 100], [235, 120], text: '', font_size: 10)} | |
<text x="238" y="112" font-size="10px">@first_line_started_from</text> | |
#{line_with_text([235, 120], [235, 140], text: '', font_size: 10)} | |
<text x="238" y="132" font-size="10px">@started_from</text> | |
EOS | |
#{line_with_text([-10, 0], [-10, -80], text: '@scroll_partial_screen', font_size: 10, edge: [2, 0])} | |
#{line_with_text([265, 120], [265, 200], text: '@rest_height', font_size: 10)} | |
<line x1="0" y1="80" x2="400" y2="80" stroke-width="1" stroke="black" /> | |
<line x1="0" y1="140" x2="400" y2="140" stroke-width="1" stroke="black" /> | |
#{line_with_text([410, 80], [410, 140], text: '@highest_in_this', font_size: 10, edge: [2, 0])} | |
#{line_with_text([430, -80], [430, 260], text: '@highest_in_all', font_size: 10, edge: [25, 0])} | |
#{line_with_text([265, 80], [265, 100], text: '', font_size: 10)} | |
<text x="268" y="92" font-size="10px">@started_from</text> | |
#{line_with_text([0, 100], [260, 100], text: '@cursor', font_size: 10, edge: [2, 0])} | |
#{line_with_text([0, 120], [400, 120], text: '@cursor_max', font_size: 10, edge: [0, 2])} | |
#{line_with_text([265, -80], [265, 80], text: '@first_line_started_from', font_size: 10)} | |
EOS | |
def screen_cache_inspect(code, offset = 0, range:) | |
wrapped_lines = colorize_concat_prompt(code, 2) | |
screen_cache = wrapped_lines.flat_map do |line| | |
prompt_size = 15 | |
prompt = line[0, prompt_size] | |
code = line[prompt_size..] | |
nil_prompt = "\e[36mnil\e[m" | |
lines = Reline::Unicode.split_by_width(prompt + code, 40).first.compact | |
inspect_item = ->(x, text) { | |
"[\e[34m#{x}\e[m, \e[34m#{Reline::Unicode.calculate_width(text, true)}\e[m, \e[31m\"\e[m#{text}\e[31m\"\e[m]" | |
} | |
lines.zip([prompt]).map do |line, prompt| | |
if prompt | |
"[#{inspect_item.call(0, prompt)}, #{inspect_item.call(prompt_size, line[prompt_size..])}]" | |
else | |
"[#{nil_prompt}, #{inspect_item.call(0, line.to_s)}]" | |
end | |
end | |
end[range] | |
lines_to_svg(screen_cache) | |
end | |
File.write 'line_editor_rendered.svg', indent_svg(<<~SVG) | |
#{svg_header(1200, 700)} | |
<defs> | |
#{short_irb_output_template} | |
#{short_irb_template} | |
#{long_irb_template} | |
</defs> | |
<g font-family="#{FONT}"> | |
<g transform="translate(40 20)"> | |
#{short_irb_view(label: true, uncontrolable_highlight: true)} | |
#{line_with_text([235, 100], [235, 0], text: '', font_size: 10)} | |
<text x="230" y="62" text-anchor="end" font-size="10px">@rendered_screen.base_y</text> | |
#{line_with_text([235, 100], [235, 140], text: '', font_size: 10)} | |
<text x="230" y="123" text-anchor="end" font-size="10px">@rendered_screen.cursor_y</text> | |
%(<line x1="0" y1="100" x2="1120" y2="100" stroke-width="1" stroke="black" />) | |
%(<line x1="0" y1="180" x2="1120" y2="180" stroke-width="1" stroke="black" />) | |
<g transform="translate(440 100)"> | |
#{screen_cache_inspect(short_code, 2, range: 0..3)} | |
<text x="0" y="-4" font-size="10px"> | |
@rendered_screen.lines | |
</text> | |
</g> | |
</g> | |
<g transform="translate(40 320)"> | |
#{long_irb_view(label: true)} | |
<text x="265" y="-7" font-size="10px" text-anchor="middle">@rendered_screen.base_y == 0</text> | |
#{line_with_text([265, 0], [265, 100], text: '', font_size: 10)} | |
<text x="260" y="63" text-anchor="end" font-size="10px">@rendered_screen.cursor_y</text> | |
%(<line x1="0" y1="0" x2="1120" y2="0" stroke-width="1" stroke="black" />) | |
%(<line x1="0" y1="200" x2="1120" y2="200" stroke-width="1" stroke="black" />) | |
<g transform="translate(440 0)"> | |
#{screen_cache_inspect(long_code, range: 4..13)} | |
<text x="0" y="-4" font-size="10px"> | |
@rendered_screen.lines | |
</text> | |
</g> | |
</g> | |
<g transform="translate(40 605)" font-size="10px"> | |
#{memo_to_svg(<<~MEMO)} | |
Uncontrolable area: Can't render here | |
Controlable area: Everything rendered is under Reline's control | |
@rendered_screen.base_y: Height of uncontrolable area | |
@rendered_screen.cursor_y: Rendered cursor row in controlable area | |
@rendered_screen.lines: Rendered text fragments (prompt, code, dialog, etc) for each line. Position may overlap. | |
@rendered_screen.lines[row_index]: Array of ([col, width, text] | nil) | |
MEMO | |
</g> | |
</g> | |
</svg> | |
SVG | |
File.write 'line_editor_buffer.svg', indent_svg(<<~SVG) | |
#{svg_header(640, 320)} | |
<defs> | |
#{short_irb_output_template} | |
#{short_irb_template} | |
</defs> | |
<g font-family="#{FONT}"> | |
<text x="298" y="20" font-size="10px" text-anchor="middle">@buffer_of_lines</text> | |
<g transform="translate(20 28)"> | |
<rect x="-2" y="-4" width="594" height="54" fill="none" stroke="black" stroke-width="1px"></rect> | |
<rect x="24" y="16" width="460" height="16" fill="silver"></rect> | |
<rect x="328" y="16" width="8" height="16" fill="gray"></rect> | |
#{lines_to_svg(short_code.lines.each_with_index.map{"\e[37m%d:\e[m %s" % [_2, _1]}, size: 16)} | |
#{line_with_text([24, 34], [328, 34], text: '', font_size: 10, edge: [0, 2])} | |
<text x="176" y="44" font-size="10px" text-anchor="middle">@byte_pointer == 38</text> | |
<text x="488" y="27" font-size="10px">@line_index == 1</text> | |
</g> | |
<g transform="translate(40 100)"> | |
#{short_irb_view(code_opacity: 1, controlable_highlight: false, background: 'none')} | |
</g> | |
</g> | |
</svg> | |
SVG | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment