330 lines
11 KiB
Ruby
330 lines
11 KiB
Ruby
class Editor
|
|
attr_accessor :text, :cursor, :selection_start, :filename
|
|
|
|
def initialize(filename, x, y, width, height)
|
|
contents = File.exist?(filename) ? File.read(filename) : "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
|
|
@text = contents.grapheme_clusters
|
|
@cursor = @text.size
|
|
@selection_start = 0
|
|
@scroll = 0
|
|
@x = x
|
|
@y = y
|
|
@width = width
|
|
@height = height
|
|
@filename = filename
|
|
@folds = []
|
|
@highlights = Array.new(@text.size)
|
|
@highlight_generation = 0
|
|
@tree = nil
|
|
@num_width = (@text.count("\n") + 1).to_s.size + 3
|
|
|
|
@parser = C.ts_parser_new
|
|
@lang_ts = C.tree_sitter_ruby
|
|
C.ts_parser_set_language @parser, @lang_ts
|
|
@cached_tree = nil
|
|
C.load_query "/home/syed/main/crib/grammar/ruby.scm"
|
|
|
|
highlight!
|
|
|
|
rebuild_folded_lines!
|
|
end
|
|
|
|
def save!
|
|
File.write @filename, @text.join
|
|
end
|
|
|
|
def highlight!
|
|
gen = @highlight_generation
|
|
src = @text.join
|
|
text_bytes = []
|
|
byte_offset = 0
|
|
@text.each do |cluster|
|
|
text_bytes << byte_offset
|
|
byte_offset += cluster.bytesize
|
|
end
|
|
text_bytes << byte_offset
|
|
line_map = []
|
|
line = 0
|
|
@text.each_with_index do |ch, i|
|
|
line_map[i] = line
|
|
line += 1 if ch == "\n"
|
|
end
|
|
# TODO: add ts_tree_edit calls in erase/insert
|
|
count_ptr = FFI::MemoryPointer.new :uint32
|
|
@cached_tree = C.ts_parser_parse_string @parser, nil, src, src.bytesize
|
|
root = C.ts_tree_root_node @cached_tree
|
|
ptr = C.ts_collect_tokens root, count_ptr, @lang_ts, src
|
|
count = count_ptr.read_uint32
|
|
ints = ptr.read_array_of_uint32 count
|
|
C.free_tokens ptr
|
|
count_ptr.free
|
|
C.ts_tree_delete @cached_tree
|
|
return if gen != @highlight_generation
|
|
@highlight_generation = 0
|
|
@highlights = Array.new(@text.size)
|
|
done_folds = []
|
|
ints.each_slice(3).map do |start_byte, end_byte, sym_i|
|
|
start_cluster = byte_to_cluster start_byte, text_bytes
|
|
end_cluster = byte_to_cluster end_byte - 1, text_bytes
|
|
next if start_cluster.nil? || end_cluster.nil?
|
|
sym = TS_SYMBOL_MAP[sym_i]
|
|
if sym == "@fold"
|
|
start_row = line_map[start_cluster]
|
|
end_row = line_map[end_cluster]
|
|
next if start_row == end_row
|
|
hash_value = Zlib.crc32 @text[start_cluster...end_cluster].join
|
|
done_folds << hash_value
|
|
if (f = @folds.find { |f| f.crc32 == hash_value })
|
|
f.start = start_row
|
|
f.end = end_row
|
|
else
|
|
@folds << Fold.new(start_row, end_row, false, hash_value)
|
|
end
|
|
next
|
|
end
|
|
hl = TS_RUBY[sym]
|
|
next unless hl
|
|
(start_cluster..end_cluster).each do |i|
|
|
@highlights[i] = hl if @highlights[i].nil? || @highlights[i].priority < hl.priority
|
|
end
|
|
end
|
|
@folds.select! { |f| done_folds.include? f.crc32 }
|
|
rebuild_folded_lines!
|
|
end
|
|
|
|
def byte_to_cluster(byte, byte_starts)
|
|
left = 0
|
|
right = byte_starts.size - 2
|
|
while left <= right
|
|
mid = (left + right) / 2
|
|
if byte_starts[mid] <= byte && byte < byte_starts[mid + 1]
|
|
return mid
|
|
elsif byte < byte_starts[mid]
|
|
right = mid - 1
|
|
else
|
|
left = mid + 1
|
|
end
|
|
end
|
|
nil
|
|
end
|
|
|
|
def erase(x0, x1)
|
|
return if x0 < 0 || x1 < x0
|
|
lines_deleted = @text[x0...x1].count "\n"
|
|
@text[x0...x1] = []
|
|
@highlights[x0...x1] = []
|
|
@highlight_generation += 1
|
|
@folds.map! do |f|
|
|
if f.start >= @text[...x0].count("\n")
|
|
f.start -= lines_deleted
|
|
f.end -= lines_deleted
|
|
end
|
|
f
|
|
end
|
|
rebuild_folded_lines!
|
|
@num_width = (@text.count("\n") + 1).to_s.size + 3
|
|
end
|
|
|
|
def insert(x, str)
|
|
@text.insert x, *str.grapheme_clusters
|
|
@highlights.insert x, *Array.new(str.grapheme_clusters.size)
|
|
lines_added = str.count "\n"
|
|
@folds.map! do |f|
|
|
if f.start >= @text[...x].count("\n")
|
|
f.start += lines_added
|
|
f.end += lines_added
|
|
end
|
|
f
|
|
end
|
|
rebuild_folded_lines!
|
|
@highlight_generation += 1
|
|
@num_width = (@text.count("\n") + 1).to_s.size + 3
|
|
end
|
|
|
|
def move_up!
|
|
precursor_text = @text[...@cursor]
|
|
last_newline = precursor_text.rindex "\n"
|
|
return unless last_newline
|
|
@preferred_col ||= @cursor - last_newline
|
|
prev_start = precursor_text[...last_newline].rindex("\n") || -1
|
|
@cursor = [
|
|
prev_start + @preferred_col,
|
|
@text[prev_start + 1..].index("\n")&.+(prev_start + 1) || @text.size
|
|
].min
|
|
end
|
|
|
|
def move_down!
|
|
next_newline = @text[@cursor..].index("\n")&.+(@cursor)
|
|
return unless next_newline
|
|
@preferred_col ||= (@cursor - (@text[...@cursor].rindex("\n") || -1)).abs
|
|
@cursor = [
|
|
next_newline + @preferred_col,
|
|
@text[next_newline + 1..].index("\n")&.+(next_newline + 1) || @text.size
|
|
].min
|
|
end
|
|
|
|
def move_left!
|
|
@cursor = [@cursor - 1, 0].max
|
|
@preferred_col = nil
|
|
end
|
|
|
|
def move_right!
|
|
@cursor = [@cursor + 1, @text.size].min
|
|
@preferred_col = nil
|
|
end
|
|
|
|
def cursor_valid?
|
|
!@folded_lines.include? @text[0...@cursor].count("\n")
|
|
end
|
|
|
|
def handle_event(event)
|
|
return if event.nil?
|
|
case KEY_TYPE[event[:key_type]]
|
|
when :char
|
|
ch = event[:c].chr
|
|
ch = "\n" if ch == "\r"
|
|
if ch == "'"
|
|
cr = @text[0...@cursor].count("\n")
|
|
@folds.find { |f| f.active = !f.active if f.start <= cr && f.end >= cr }
|
|
rebuild_folded_lines!
|
|
elsif ch == ctrl_key('s').chr
|
|
save!
|
|
elsif ch =~ /[[:print:]]|\n/
|
|
insert @cursor, ch
|
|
@cursor += 1
|
|
elsif ch == "\b" || ch == "\x7f"
|
|
erase @cursor - 1, @cursor
|
|
@cursor = [@cursor - 1, 0].max
|
|
end
|
|
@preferred_col = nil
|
|
when :special
|
|
return unless MODIFIER[event[:special_modifier]] == :none
|
|
case SPECIAL_KEY[event[:special_key]]
|
|
when :up
|
|
move_up!
|
|
move_up! until cursor_valid?
|
|
when :down
|
|
move_down!
|
|
move_down! until cursor_valid?
|
|
when :left
|
|
move_left!
|
|
move_left! until cursor_valid?
|
|
when :right
|
|
move_right!
|
|
move_right! until cursor_valid?
|
|
when :delete
|
|
erase @cursor, @cursor + 1
|
|
@cursor = [@cursor, @text.size].min
|
|
@preferred_col = nil
|
|
end
|
|
end
|
|
move_right! until cursor_valid?
|
|
adjust_scroll!
|
|
end
|
|
|
|
def adjust_scroll!
|
|
cr = cursor_row_visual
|
|
start = @scroll
|
|
last = @scroll + @height - 1
|
|
if cr < start
|
|
@scroll = cr
|
|
elsif cr > last
|
|
@scroll = cr - @height + 1
|
|
end
|
|
end
|
|
|
|
def cursor_row_visual
|
|
row = col = 0
|
|
nc = 0
|
|
@text.each_with_index do |ch, i|
|
|
return row if i == @cursor
|
|
if @folded_lines.include? nc
|
|
nc += 1 if ch == "\n"
|
|
next
|
|
end
|
|
if ch == "\n"
|
|
row += 1
|
|
nc += 1
|
|
col = 0
|
|
next
|
|
end
|
|
w = C.display_width(ch)
|
|
if col + w > @width - @num_width
|
|
row += 1
|
|
col = 0
|
|
end
|
|
col += w
|
|
end
|
|
row
|
|
end
|
|
|
|
def rebuild_folded_lines!
|
|
@folded_lines = Set.new
|
|
@folds.each do |f|
|
|
(f.start + 1..f.end).each { |line_num| @folded_lines.add(line_num) } if f.active
|
|
end
|
|
end
|
|
|
|
def render
|
|
(0...@height).each { |h| (0...@width).each { |w|
|
|
C.update @y + h, @x + w, ' ', 0x000000, 0x000000, 0
|
|
} }
|
|
row = col = nc = 0
|
|
cursor_row = cursor_col = nil
|
|
@text.each_with_index do |ch, i|
|
|
if @folded_lines.include? nc
|
|
nc += 1 if ch == "\n"
|
|
next
|
|
end
|
|
if i == @cursor
|
|
cursor_row = row
|
|
cursor_col = col
|
|
end
|
|
if col == 0
|
|
color = 0x666666
|
|
color = 0xFFFFFF if cursor_row == row
|
|
num = (nc + 1).to_s.rjust @num_width - 1, '👪👩💻👨🏿🚀🏳️🌈🔥🛠️🌍🥶✅❌💡⏰🚀'#ddasasdasda
|
|
num.each_char.with_index do |c, j|
|
|
C.update @y + row - @scroll, @x + j, c, color, 0x000000, 0
|
|
end
|
|
if (fold = @folds.find { |f| nc == f.start })
|
|
icon = fold.active ? "" : "" # "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : "" "" : ""
|
|
C.update @y + row - @scroll, @x, icon, 0xFF0000, 0x000000, 0
|
|
end
|
|
end
|
|
if ch == "\n"
|
|
row += 1
|
|
nc += 1
|
|
col = 0
|
|
next
|
|
end
|
|
w = C.display_width(ch)
|
|
if col + w > @width - @num_width
|
|
row += 1
|
|
col = 0
|
|
end
|
|
col += w
|
|
next if row < @scroll
|
|
break if row - @scroll >= @height
|
|
fg = 0xFFFFFF
|
|
bg = 0x000000
|
|
fl = 0
|
|
if (h = @highlights[i])
|
|
fg = h.color_fg
|
|
bg = h.color_bg
|
|
fl = h.flags
|
|
end
|
|
C.update @y + row - @scroll, @x + @num_width + col - w, ch, fg, bg, fl
|
|
end
|
|
if @text[-1] == "\n" || @text.size == 0
|
|
num = (nc + 1).to_s.rjust @num_width - 1, ' '
|
|
color = 0x666666
|
|
color = 0xFFFFFF if (cursor_row || row) == row
|
|
num.each_char.with_index do |c, j|
|
|
C.update @y + row - @scroll, @x + j, c, color, 0x000000, 0
|
|
end
|
|
end
|
|
C.set_cursor @y - @scroll + (cursor_row || row), @x + @num_width + (cursor_col || col), 1
|
|
end
|
|
end
|