Switch to c++

This commit is contained in:
2025-12-07 17:22:12 +00:00
parent 2927df7710
commit 7acf35c8ea
55 changed files with 3506 additions and 304 deletions

5
.gitignore vendored
View File

@@ -7,7 +7,10 @@
.vscode
tests/tmp_*
tests/*
!tests/.keep
builds/*
!builds/.keep
src/cpp/__tmp__

80
Makefile Normal file
View File

@@ -0,0 +1,80 @@
SRC_DIR := src
BIN_DIR := bin
OBJ_DIR := build
TARGET_DEBUG := $(BIN_DIR)/crib-dbg
TARGET_RELEASE := $(BIN_DIR)/crib
CXX_DEBUG := g++
CXX_RELEASE := clang++
CFLAGS_DEBUG := -std=c++20 -Wall -Wextra -O0 -g -fno-inline -gsplit-dwarf
CFLAGS_RELEASE := -std=c++20 -O3 -march=native -flto=thin \
-fno-exceptions -fno-rtti -fstrict-aliasing -ffast-math -funroll-loops \
-fomit-frame-pointer -DNDEBUG -s \
-mllvm -inline-threshold=10000 \
-mllvm -vectorize-loops \
-mllvm -force-vector-width=8 \
-mllvm -unroll-threshold=500000
UNICODE_SRC := $(wildcard libs/unicode_width/*.c)
UNICODE_OBJ_DEBUG := $(patsubst libs/unicode_width/%.c,$(OBJ_DIR)/debug/unicode_width/%.o,$(UNICODE_SRC))
UNICODE_OBJ_RELEASE := $(patsubst libs/unicode_width/%.c,$(OBJ_DIR)/release/unicode_width/%.o,$(UNICODE_SRC))
LIBS := \
libs/libgrapheme/libgrapheme.a \
libs/tree-sitter/libtree-sitter.a \
libs/tree-sitter-ruby/libtree-sitter-ruby.a \
-lpcre2-8
SRC := $(wildcard $(SRC_DIR)/*.cc)
OBJ_DEBUG := $(patsubst $(SRC_DIR)/%.cc,$(OBJ_DIR)/debug/%.o,$(SRC))
OBJ_RELEASE := $(patsubst $(SRC_DIR)/%.cc,$(OBJ_DIR)/release/%.o,$(SRC))
DEP_DEBUG := $(OBJ_DEBUG:.o=.d)
DEP_RELEASE := $(OBJ_RELEASE:.o=.d)
.PHONY: all test release clean
all: debug
test: $(TARGET_DEBUG)
release: $(TARGET_RELEASE)
$(TARGET_DEBUG): $(OBJ_DEBUG) $(UNICODE_OBJ_DEBUG)
mkdir -p $(BIN_DIR)
$(CXX_DEBUG) $(CFLAGS_DEBUG) -o $@ $^ $(LIBS)
$(TARGET_RELEASE): $(OBJ_RELEASE) $(UNICODE_OBJ_RELEASE)
mkdir -p $(BIN_DIR)
$(CXX_RELEASE) $(CFLAGS_RELEASE) -o $@ $^ $(LIBS)
# Pattern rules for object files + dependency generation
$(OBJ_DIR)/debug/%.o: $(SRC_DIR)/%.cc
mkdir -p $(dir $@)
$(CXX_DEBUG) $(CFLAGS_DEBUG) -MMD -MP -c $< -o $@
$(OBJ_DIR)/release/%.o: $(SRC_DIR)/%.cc
mkdir -p $(dir $@)
$(CXX_RELEASE) $(CFLAGS_RELEASE) -MMD -MP -c $< -o $@
$(OBJ_DIR)/debug/unicode_width/%.o: libs/unicode_width/%.c
mkdir -p $(dir $@)
$(CXX_DEBUG) $(CFLAGS_DEBUG) -MMD -MP -c $< -o $@
$(OBJ_DIR)/release/unicode_width/%.o: libs/unicode_width/%.c
mkdir -p $(dir $@)
$(CXX_RELEASE) $(CFLAGS_RELEASE) -MMD -MP -c $< -o $@
# Include deps if they exist
DEP_DEBUG += $(UNICODE_OBJ_DEBUG:.o=.d)
DEP_RELEASE += $(UNICODE_OBJ_RELEASE:.o=.d)
-include $(DEP_DEBUG)
-include $(DEP_RELEASE)
clean:
rm -rf $(OBJ_DIR) $(BIN_DIR)

54
__old__/crib.rb Executable file
View File

@@ -0,0 +1,54 @@
#!/usr/bin/env ruby
require "ffi"
require "zlib"
require_relative "../src/ruby/utils"
require_relative "../src/ruby/mod"
require_relative "../src/ruby/ts_rb"
require_relative "../src/ruby/fm"
require_relative "../src/ruby/editor"
require_relative "../src/ruby/ide"
$rows, $cols = C.start_screen
$running = true
$event_queue = Queue.new
$folder = Dir.new File.dirname(ARGV[0] || Dir.pwd)
$threads = []
at_exit do
IDE.close
C.end_screen
puts "Exiting crib.rb"
end
IDE.start
$threads << Thread.new do
loop do
sleep 1.0 / 64
break unless $running
IDE.handle_event $event_queue.pop timeout: 0 until $event_queue.empty?
IDE.render
C.render
end
end
$threads << Thread.new do
loop do
sleep 1.0 / 64
break unless $running
IDE.work!
end
end
$threads << Thread.new do
loop do
break unless $running
event = C.read_key # read_key is blocking
$running = false if KEY_TYPE[event[:key_type]] == :char && event[:c] == ctrl_key('q')
$event_queue << event
end
end
$threads.each &:join

329
__old__/editor.rb Normal file
View File

@@ -0,0 +1,329 @@
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

30
__old__/fm.rb Normal file
View File

@@ -0,0 +1,30 @@
module FileManager
@hidden = true
module_function
def start(x, y, width, height)
@hidden = false
@x = x
@y = y
@width = width
@height = height
end
def toggle!
@hidden = !@hidden
end
def render
return if @hidden
(0...@height).each { |h| (0...@width).each { |w|
C.update @y + h, @x + w, ' ', 0x000000, 0x000000, 0
} }
files = $folder.children
files.each_with_index do |f, i|
f.each_char.with_index do |c, j|
C.update @y + i, @x + j, c, 0xFFFFFF, 0x000000, 0
end
end
end
end

36
__old__/ide.rb Normal file
View File

@@ -0,0 +1,36 @@
module IDE
FM_WIDTH = 20
module_function
def start
@editors = {}
@u_c = 0
editor = Editor.new (ARGV[0] || ""), FM_WIDTH, 0, $cols - FM_WIDTH, $rows
@editors[editor.filename || "untitled #{@u_c += 1}"] = editor
@selected_editor = @editors.keys.first
@focus = :editor
FileManager.start 0, 0, FM_WIDTH, $rows
end
def handle_event(event)
if @focus == :editor
@editors[@selected_editor].handle_event event if @editors.key? @selected_editor
elsif @focus == :file_manager
# TODO
end
end
def render
@editors[@selected_editor].render if @editors.key? @selected_editor
FileManager.render
end
def work!
@editors[@selected_editor].highlight! if @editors.key? @selected_editor
end
def close
# TODO
end
end

126
__old__/mod.rb Normal file
View File

@@ -0,0 +1,126 @@
module C
extend FFI::Library
ffi_lib File.join(__dir__, "../../libs/tree-sitter/libtree-sitter.so")
# TSTree and TSParser are opaque pointers
typedef :pointer, :tstree
typedef :pointer, :tsparser
# Define TSNode struct
class TSNode < FFI::Struct
layout :context, [:uint32, 4],
:id, :pointer,
:tree, :tstree
end
class TSPoint < FFI::Struct
layout :row, :uint32,
:column, :uint32
end
class TSInputEdit < FFI::Struct
layout :start_byte, :uint32,
:old_end_byte, :uint32,
:new_end_byte, :uint32,
:start_point, TSPoint.by_value,
:old_end_point, TSPoint.by_value,
:new_end_point, TSPoint.by_value
end
class TSTreeCursor < FFI::Struct
layout :tree, :pointer,
:id, :pointer,
:context, [:uint32, 3]
end
attach_function :ts_parser_new, [], :tsparser
attach_function :ts_parser_set_language, [:tsparser, :pointer], :bool
attach_function :ts_parser_parse_string, [:tsparser, :tstree, :string, :size_t], :tstree, blocking: true
attach_function :ts_tree_delete, [:tstree], :void
attach_function :ts_node_start_byte, [TSNode.by_value], :uint32
attach_function :ts_node_end_byte, [TSNode.by_value], :uint32
attach_function :ts_node_symbol, [TSNode.by_value], :uint16
attach_function :ts_tree_edit, [:tstree, TSInputEdit.by_ref], :void
attach_function :ts_tree_root_node, [:tstree], TSNode.by_value, blocking: true
attach_function :ts_node_child_count, [TSNode.by_value], :uint32
attach_function :ts_node_child, [TSNode.by_value, :uint32], TSNode.by_value
# Ruby grammar
ffi_lib File.join(__dir__, "../../libs/tree-sitter-ruby/libtree-sitter-ruby.so")
attach_function :tree_sitter_ruby, [], :pointer
ffi_lib File.join(__dir__, "../../builds/C-crib.so")
class KeyEvent < FFI::Struct
layout :key_type, :uint8,
:c, :char,
:special_key, :uint8,
:special_modifier, :uint8,
:mouse_x, :uint8,
:mouse_y, :uint8,
:mouse_button, :uint8,
:mouse_state, :uint8,
:mouse_direction, :uint8,
:mouse_modifier, :uint8
def to_s
case KEY_TYPE[self[:key_type]]
when :char
"#<KeyEvent char=#{self[:c].inspect}>"
when :special
"#<KeyEvent special key=#{SPECIAL_KEY[self[:special_key]]} mod=#{MODIFIER[self[:special_modifier]]}>"
when :mouse
"#<KeyEvent mouse x=#{self[:mouse_x]} y=#{self[:mouse_y]} " \
"btn=#{MOUSE_BUTTON[self[:mouse_button]]} state=#{MOUSE_STATE[self[:mouse_state]]} " \
"scroll_dir=#{SCROLL_DIR[self[:mouse_direction]]} mod=#{MODIFIER[self[:mouse_modifier]]}>"
else
"#<KeyEvent type=#{self[:key_type]} unknown=true>"
end
end
end
class Coords < FFI::Struct
layout :x, :int,
:y, :int
def to_ary
[self[:x], self[:y]]
end
def to_s
"#<Coords x=#{self[:x]} y=#{self[:y]}>"
end
end
class Size < FFI::Struct
layout :rows, :int,
:cols, :int
def to_ary
[self[:rows], self[:cols]]
end
def to_s
"#<Size rows=#{self[:rows]} cols=#{self[:cols]}>"
end
end
attach_function :load_query, [:string], :void
attach_function :ts_collect_tokens, [TSNode.by_value, :pointer, :pointer, :string], :pointer, blocking: true
attach_function :free_tokens, [:pointer], :void, blocking: true
attach_function :start_screen, [], Size.by_value
attach_function :end_screen, [], :void
attach_function :update, [:int, :int, :string, :uint32, :uint32, :uint8], :void
attach_function :render, [], :void, blocking: true
attach_function :set_cursor, [:int, :int, :int], :void
attach_function :read_key, [], KeyEvent.by_value, blocking: true
attach_function :get_size, [], Coords.by_value
attach_function :display_width, [:string], :int, blocking: true
end

57
__old__/ts_rb.rb Normal file
View File

@@ -0,0 +1,57 @@
query = File.read "/home/syed/main/crib/grammar/ruby.scm"
raw = query.scan(/@[a-zA-Z0-9_.]+/)
seen = {}
ordered = []
raw.each do |c|
next if seen[c]
seen[c] = true
ordered << c
end
TS_SYMBOL_MAP = ordered.freeze
spawn "echo \"#{TS_SYMBOL_MAP.sort.join "\n"}\" > /tmp/gg"
TS_RUBY = {
"@string.special.symbol" => Highlight.new(0xbd9ae6, 0x000000, CF_NONE, 2),
"@comment" => Highlight.new(0xAAAAAA, 0x000000, CF_ITALIC, 1),
"@boolean.true" => Highlight.new(0x51eeba, 0x000000, CF_NONE, 1),
"@boolean.false" => Highlight.new(0xee513a, 0x000000, CF_NONE, 1),
"@constant.nil" => Highlight.new(0xee8757, 0x000000, CF_NONE, 1),
"@constant" => Highlight.new(0xebda8c, 0x000000, CF_NONE, 1),
"@number" => Highlight.new(0xebda8c, 0x000000, CF_NONE, 2),
"@number.float" => Highlight.new(0xebda8c, 0x000000, CF_NONE, 2),
"@constant.builtin" => Highlight.new(0xfbb152, 0x000000, CF_NONE, 2),
"@punctuation.bracket" => Highlight.new(0xbd9ae6, 0x000000, CF_NONE, 1),
"@operator.ligature" => Highlight.new(0xffffff, 0x000000, CF_ITALIC, 1),
"@operator" => Highlight.new(0xffffff, 0x000000, CF_NONE, 1),
"@punctuation.delimiter" => Highlight.new(0xbd9ae6, 0x000000, CF_NONE, 1),
"@punctuation.special" => Highlight.new(0xe6a24c, 0x000000, CF_NONE, 1),
"@function" => Highlight.new(0xaad84c, 0x000000, CF_NONE, 1),
"@function.builtin" => Highlight.new(0xaad84c, 0xFF0000, CF_NONE, 1),
"@keyword.import" => Highlight.new(0xfbb152, 0x000000, CF_NONE, 1),
"@function.call" => Highlight.new(0xff5689, 0x000000, CF_NONE, 1),
"@keyword" => Highlight.new(0xfbb152, 0x000000, CF_NONE, 1),
"@keyword.conditional" => Highlight.new(0xfbb152, 0x000000, CF_NONE, 1),
"@keyword.control" => Highlight.new(0xfbb152, 0x000000, CF_NONE, 1),
"@keyword.directive" => Highlight.new(0xfbb152, 0x000000, CF_NONE, 1),
"@keyword.exception" => Highlight.new(0xfbb152, 0x000000, CF_NONE, 1),
"@keyword.function" => Highlight.new(0xfbb152, 0x000000, CF_NONE, 1),
"@keyword.operator" => Highlight.new(0xfbb152, 0x000000, CF_NONE, 1),
"@keyword.repeat" => Highlight.new(0xfbb152, 0x000000, CF_NONE, 1),
"@keyword.return" => Highlight.new(0xfbb152, 0x000000, CF_NONE, 1),
"@keyword.type" => Highlight.new(0xfbb152, 0x000000, CF_NONE, 1),
"@label" => Highlight.new(0xfbb152, 0x000000, CF_NONE, 1),
"@string" => Highlight.new(0xaad84c, 0x000000, CF_NONE, 1),
"@string.escape" => Highlight.new(0xe6a24c, 0x000000, CF_NONE, 2),
"@string.regexp" => Highlight.new(0xe6a24c, 0x000000, CF_NONE, 2),
"@type" => Highlight.new(0xaad84c, 0x000000, CF_NONE, 1),
"@variable" => Highlight.new(0xffffff, 0x000000, CF_NONE, 1),
"@variable.builtin" => Highlight.new(0xffffff, 0x000000, CF_NONE, 1),
"@variable.member" => Highlight.new(0xffffff, 0x000000, CF_NONE, 1),
"@variable.parameter" => Highlight.new(0xffffff, 0x000000, CF_NONE, 1),
}

View File

@@ -1,3 +1,12 @@
Fold = Struct.new(:start, :end, :active, :crc32)
Highlight = Struct.new(:color_fg, :color_bg, :flags, :priority)
Token = Struct.new(:start, :end, :sym)
CF_NONE = 0b00000000
CF_ITALIC = 0b00000001
CF_BOLD = 0b00000010
CF_UNDERLINE = 0b00000100
def ctrl_key(k)
k.ord & 0x1F
end
@@ -6,8 +15,9 @@ end
KEY_TYPE = {
0 => :char,
1 => :special,
2 => :mouse
}
2 => :mouse,
3 => :none
}.freeze
# Special keys
SPECIAL_KEY = {
@@ -16,10 +26,10 @@ SPECIAL_KEY = {
2 => :left,
3 => :right,
4 => :delete
}
}.freeze
# Control key
KEY_ESC = "\x1b"
KEY_ESC = "\x1b".freeze
# Mouse states
MOUSE_STATE = {
@@ -27,7 +37,7 @@ MOUSE_STATE = {
1 => :release,
2 => :drag,
3 => :scroll
}
}.freeze
# Mouse buttons
MOUSE_BUTTON = {
@@ -36,7 +46,7 @@ MOUSE_BUTTON = {
2 => :right,
3 => :scroll,
4 => :none
}
}.freeze
# Scroll directions
SCROLL_DIR = {
@@ -45,12 +55,13 @@ SCROLL_DIR = {
2 => :left,
3 => :right,
4 => :none
}
}.freeze
# Modifiers
MODIFIER = {
0 => :none,
1 => :alt,
2 => :cntrl,
3 => :cntrl_alt,
4 => :shift
}
}.freeze

BIN
bin/crib Executable file

Binary file not shown.

BIN
bin/crib-dbg Executable file

Binary file not shown.

14
build/debug/editor.d Normal file
View File

@@ -0,0 +1,14 @@
build/debug/editor.o: src/editor.cc src/../libs/libgrapheme/grapheme.h \
src/../include/editor.h \
src/../include/../libs/tree-sitter/lib/include/tree_sitter/api.h \
src/../include/./rope.h src/../include/./ui.h src/../include/./utils.h \
src/../include/ts.h \
src/../libs/tree-sitter-ruby/bindings/c/tree-sitter-ruby.h
src/../libs/libgrapheme/grapheme.h:
src/../include/editor.h:
src/../include/../libs/tree-sitter/lib/include/tree_sitter/api.h:
src/../include/./rope.h:
src/../include/./ui.h:
src/../include/./utils.h:
src/../include/ts.h:
src/../libs/tree-sitter-ruby/bindings/c/tree-sitter-ruby.h:

BIN
build/debug/editor.dwo Normal file

Binary file not shown.

2
build/debug/input.d Normal file
View File

@@ -0,0 +1,2 @@
build/debug/input.o: src/input.cc src/../include/ui.h
src/../include/ui.h:

BIN
build/debug/input.dwo Normal file

Binary file not shown.

12
build/debug/main.d Normal file
View File

@@ -0,0 +1,12 @@
build/debug/main.o: src/main.cc src/../include/editor.h \
src/../include/../libs/tree-sitter/lib/include/tree_sitter/api.h \
src/../include/./rope.h src/../include/./ui.h src/../include/./utils.h \
src/../include/ts.h \
src/../libs/tree-sitter/lib/include/tree_sitter/api.h
src/../include/editor.h:
src/../include/../libs/tree-sitter/lib/include/tree_sitter/api.h:
src/../include/./rope.h:
src/../include/./ui.h:
src/../include/./utils.h:
src/../include/ts.h:
src/../libs/tree-sitter/lib/include/tree_sitter/api.h:

BIN
build/debug/main.dwo Normal file

Binary file not shown.

6
build/debug/renderer.d Normal file
View File

@@ -0,0 +1,6 @@
build/debug/renderer.o: src/renderer.cc \
src/../libs/libgrapheme/grapheme.h \
src/../libs/unicode_width/unicode_width.h src/../include/ui.h
src/../libs/libgrapheme/grapheme.h:
src/../libs/unicode_width/unicode_width.h:
src/../include/ui.h:

BIN
build/debug/renderer.dwo Normal file

Binary file not shown.

2
build/debug/rope.d Normal file
View File

@@ -0,0 +1,2 @@
build/debug/rope.o: src/rope.cc src/../include/rope.h
src/../include/rope.h:

BIN
build/debug/rope.dwo Normal file

Binary file not shown.

11
build/debug/ts.d Normal file
View File

@@ -0,0 +1,11 @@
build/debug/ts.o: src/ts.cc src/../include/ts.h src/../include/./editor.h \
src/../include/./../libs/tree-sitter/lib/include/tree_sitter/api.h \
src/../include/././rope.h src/../include/././ui.h \
src/../include/././utils.h src/../include/rope.h
src/../include/ts.h:
src/../include/./editor.h:
src/../include/./../libs/tree-sitter/lib/include/tree_sitter/api.h:
src/../include/././rope.h:
src/../include/././ui.h:
src/../include/././utils.h:
src/../include/rope.h:

BIN
build/debug/ts.dwo Normal file

Binary file not shown.

View File

@@ -0,0 +1,3 @@
build/debug/unicode_width/unicode_width.o: \
libs/unicode_width/unicode_width.c libs/unicode_width/unicode_width.h
libs/unicode_width/unicode_width.h:

Binary file not shown.

4
build/debug/utils.d Normal file
View File

@@ -0,0 +1,4 @@
build/debug/utils.o: src/utils.cc src/../libs/libgrapheme/grapheme.h \
src/../include/utils.h
src/../libs/libgrapheme/grapheme.h:
src/../include/utils.h:

BIN
build/debug/utils.dwo Normal file

Binary file not shown.

15
build/release/editor.d Normal file
View File

@@ -0,0 +1,15 @@
build/release/editor.o: src/editor.cc src/../libs/libgrapheme/grapheme.h \
src/../include/editor.h \
src/../include/../libs/tree-sitter/lib/include/tree_sitter/api.h \
src/../include/./rope.h src/../include/./ui.h src/../include/./utils.h \
src/../include/ts.h src/../include/./editor.h \
src/../libs/tree-sitter-ruby/bindings/c/tree-sitter-ruby.h
src/../libs/libgrapheme/grapheme.h:
src/../include/editor.h:
src/../include/../libs/tree-sitter/lib/include/tree_sitter/api.h:
src/../include/./rope.h:
src/../include/./ui.h:
src/../include/./utils.h:
src/../include/ts.h:
src/../include/./editor.h:
src/../libs/tree-sitter-ruby/bindings/c/tree-sitter-ruby.h:

2
build/release/input.d Normal file
View File

@@ -0,0 +1,2 @@
build/release/input.o: src/input.cc src/../include/ui.h
src/../include/ui.h:

14
build/release/main.d Normal file
View File

@@ -0,0 +1,14 @@
build/release/main.o: src/main.cc src/../include/editor.h \
src/../include/../libs/tree-sitter/lib/include/tree_sitter/api.h \
src/../include/./rope.h src/../include/./ui.h src/../include/./utils.h \
src/../include/ts.h src/../include/./editor.h src/../include/ui.h \
src/../libs/tree-sitter/lib/include/tree_sitter/api.h
src/../include/editor.h:
src/../include/../libs/tree-sitter/lib/include/tree_sitter/api.h:
src/../include/./rope.h:
src/../include/./ui.h:
src/../include/./utils.h:
src/../include/ts.h:
src/../include/./editor.h:
src/../include/ui.h:
src/../libs/tree-sitter/lib/include/tree_sitter/api.h:

6
build/release/renderer.d Normal file
View File

@@ -0,0 +1,6 @@
build/release/renderer.o: src/renderer.cc \
src/../libs/libgrapheme/grapheme.h \
src/../libs/unicode_width/unicode_width.h src/../include/ui.h
src/../libs/libgrapheme/grapheme.h:
src/../libs/unicode_width/unicode_width.h:
src/../include/ui.h:

2
build/release/rope.d Normal file
View File

@@ -0,0 +1,2 @@
build/release/rope.o: src/rope.cc src/../include/rope.h
src/../include/rope.h:

14
build/release/ts.d Normal file
View File

@@ -0,0 +1,14 @@
build/release/ts.o: src/ts.cc src/../include/ts.h \
src/../include/./editor.h \
src/../include/./../libs/tree-sitter/lib/include/tree_sitter/api.h \
src/../include/././rope.h src/../include/././ui.h \
src/../include/././utils.h src/../include/editor.h \
src/../include/rope.h
src/../include/ts.h:
src/../include/./editor.h:
src/../include/./../libs/tree-sitter/lib/include/tree_sitter/api.h:
src/../include/././rope.h:
src/../include/././ui.h:
src/../include/././utils.h:
src/../include/editor.h:
src/../include/rope.h:

View File

@@ -0,0 +1,3 @@
build/release/unicode_width/unicode_width.o: \
libs/unicode_width/unicode_width.c libs/unicode_width/unicode_width.h
libs/unicode_width/unicode_width.h:

4
build/release/utils.d Normal file
View File

@@ -0,0 +1,4 @@
build/release/utils.o: src/utils.cc src/../libs/libgrapheme/grapheme.h \
src/../include/utils.h
src/../libs/libgrapheme/grapheme.h:
src/../include/utils.h:

View File

@@ -1,11 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
DIR="$(cd -- "$(dirname -- "$0")" && pwd)"
mkdir -p "$DIR/builds"
g++ -O2 -std=c++20 -shared -fPIC -Wall -Wextra \
-o "$DIR/builds/C-crib.so" $DIR/src/cpp/*.cpp
strip "$DIR/builds/C-crib.so"

53
crib.rb
View File

@@ -1,53 +0,0 @@
#!/usr/bin/env ruby
require_relative "./src/ruby/mod"
# C.start_screen
#
# at_exit do
# C.end_screen
# puts "bye"
# end
#
#
# class Tester
# @current_row = 0
#
# class << self
# def debug_print(text)
# text.each_char.with_index do |ch, i|
# C.update(@current_row, i, ch, 0xFFFFFE, 0x000001, 0)
# end
# @current_row += 1
# end
#
# def reset
# @current_row = 0
# end
# end
# end
#
# render_thread = Thread.new do
# loop do
# sleep(1.0/20)
# C.render
# end
# end
#
# loop do
# sleep 0.001
# event = C.read_key
# break if event[:key_type] == 0 && event[:c] == 'q'.ord
# Tester.reset if event[:key_type] == 0 && event[:c] == 'r'.ord
# Tester.debug_print(C.get_size.to_s) if event[:key_type] == 0 && event[:c] == 's'.ord
# Tester.debug_print(event.to_s)
# end
#
# render_thread.kill
# render_thread.join
require_relative("./src/ruby/editor.rb")
test = Buffer.new("hello world\nwow")
pp test.render_text(0, 0, 10, 10)

346
grammar/ruby.scm Normal file
View File

@@ -0,0 +1,346 @@
[
(method)
(singleton_method)
(class)
(module)
(if)
(else)
(case)
(when)
(in)
(do_block)
(singleton_class)
(heredoc_content)
(lambda)
] @fold
;; #ffffff #000000 0 0 0 1
[
(identifier)
(global_variable)
] @variable
;; #fbb152 #000000 0 0 0 1
[
"alias"
"begin"
"do"
"end"
"ensure"
"module"
"rescue"
"then"
] @keyword
;; #fbb152 #000000 0 0 0 1
"class" @keyword.type
;; #fbb152 #000000 0 0 0 1
[
"return"
"yield"
] @keyword.return
;; #fbb152 #000000 0 0 0 1
[
"and"
"or"
"in"
"not"
] @keyword.operator
;; #fbb152 #000000 0 0 0 1
[
"def"
"undef"
] @keyword.function
(method
"end" @keyword.function)
;; #fbb152 #000000 0 0 0 1
[
"case"
"else"
"elsif"
"if"
"unless"
"when"
"then"
] @keyword.conditional
(if
"end" @keyword.conditional)
;; #fbb152 #000000 0 0 0 1
[
"for"
"until"
"while"
"break"
"redo"
"retry"
"next"
] @keyword.repeat
;; #ebda8c #000000 0 0 0 1
(constant) @constant
;; #fbb152 #000000 0 0 0 1
[
"rescue"
"ensure"
] @keyword.exception
;; #aad84c #000000 0 0 0 1
"defined?" @function
;; #aad84c #000000 0 0 0 1
(call
receiver: (constant)? @type
method: [
(identifier)
(constant)
;; #ff5689 #000000 0 0 0 1
] @function.call)
(alias
(identifier) @function)
(setter
(identifier) @function)
(method
name: [
(identifier) @function
(constant) @type
])
(singleton_method
name: [
(identifier) @function
(constant) @type
])
(class
name: (constant) @type)
(module
name: (constant) @type)
(superclass
(constant) @type)
;; #ffffff #000000 0 0 0 1
[
(class_variable)
(instance_variable)
] @variable.member
((identifier) @keyword.modifier
(#match? @keyword.modifier "^(private|protected|public)$" ))
;; #fbb152 #000000 0 0 0 1
(program
(call
(identifier) @keyword.import)
(#match? @keyword.import "^(require|require_relative|load)$"))
;; #fbb152 #000000 0 0 0 2
((identifier) @constant.builtin
(#match? @constant.builtin "^(__callee__|__dir__|__id__|__method__|__send__|__ENCODING__|__FILE__|__LINE__)$" ))
;; #aad84c #000000 0 0 0 1
((identifier) @function.builtin
(#match? @function.builtin "^(attr_reader|attr_writer|attr_accessor|module_function)$" ))
((call
!receiver
method: (identifier) @function.builtin)
(#match? @function.builtin "^(include|extend|prepend|refine|using)"))
((identifier) @keyword.exception
(#match? @keyword.exception "^(raise|fail|catch|throw)" ))
;; #ffffff #000000 0 0 0 1
[
(self)
(super)
] @variable.builtin
;; #ffffff #000000 0 0 0 1
(method_parameters
(identifier) @variable.parameter)
(lambda_parameters
(identifier) @variable.parameter)
(block_parameters
(identifier) @variable.parameter)
(splat_parameter
(identifier) @variable.parameter)
(hash_splat_parameter
(identifier) @variable.parameter)
(optional_parameter
(identifier) @variable.parameter)
(destructured_parameter
(identifier) @variable.parameter)
(block_parameter
(identifier) @variable.parameter)
(keyword_parameter
(identifier) @variable.parameter)
;; #aad84c #000000 0 0 0 1
[
(string_content)
(heredoc_content)
"\""
"`"
] @string
;; #fbb152 #000000 0 0 0 1
[
(heredoc_beginning)
(heredoc_end)
] @label
;; #bd9ae6 #000000 0 0 0 2
[
(bare_symbol)
(simple_symbol)
(delimited_symbol)
(hash_key_symbol)
] @string.special.symbol
;; #e6a24c #000000 0 0 0 2
(regex
(string_content) @string.regexp)
;; #e6a24c #000000 0 0 0 2
(escape_sequence) @string.escape
;; #ebda8c #000000 0 0 0 2
(integer) @number
;; #ebda8c #000000 0 0 0 2
(float) @number.float
;; #51eeba #000000 0 0 0 1
(true) @boolean.true
;; #ee513a #000000 0 0 0 1
(false) @boolean.false
;; #ee8757 #000000 0 0 0 1
(nil) @constant.nil
;; #AAAAAA #000000 0 1 0 1
(comment) @comment
(program
(comment)+ @comment.documentation
(class))
(module
(comment)+ @comment.documentation
(body_statement
(class)))
(class
(comment)+ @comment.documentation
(body_statement
(method)))
(body_statement
(comment)+ @comment.documentation
(method))
;; #ffffff #000000 0 0 0 1
[
"!"
"="
">>"
"<<"
">"
"<"
"**"
"*"
"/"
"%"
"+"
"-"
"&"
"|"
"^"
"%="
"+="
"-="
"*="
"/="
"=~"
"!~"
"?"
":"
] @operator
;; #ffffff #000000 0 1 0 1
[
"=="
"==="
"<=>"
"=>"
"->"
">="
"<="
"||"
"||="
"&&="
"&&"
"!="
".."
"..."
] @operator.ligature
;; #bd9ae6 #000000 0 0 0 1
[
","
";"
"."
"&."
"::"
] @punctuation.delimiter
;; #bd9ae6 #000000 0 0 0 1
(regex
"/" @punctuation.bracket)
(pair
":" @punctuation.delimiter)
[
"("
")"
"["
"]"
"{"
"}"
"%w("
"%i("
] @punctuation.bracket
(block_parameters
"|" @punctuation.bracket)
;; #e6a24c #000000 0 0 0 1
(interpolation
"#{" @punctuation.special
"}" @punctuation.special)

View File

111
include/editor.h Normal file
View File

@@ -0,0 +1,111 @@
#pragma once
#include "../libs/tree-sitter/lib/include/tree_sitter/api.h"
#include "./rope.h"
#include "./ui.h"
#include "./utils.h"
#include <cstdint>
#include <map>
#include <shared_mutex>
struct Highlight {
uint32_t fg;
uint32_t bg;
uint32_t flags;
uint8_t priority;
};
struct Span {
uint32_t start;
uint32_t end;
Highlight *hl;
bool operator<(const Span &other) const { return start < other.start; }
};
struct SpanCursor {
const std::vector<Span> &spans;
size_t index = 0;
std::vector<Span *> active;
SpanCursor(const std::vector<Span> &s) : spans(s) {}
Highlight *get_highlight(uint32_t byte_offset) {
for (int i = active.size() - 1; i >= 0; i--)
if (active[i]->end <= byte_offset)
active.erase(active.begin() + i);
while (index < spans.size() && spans[index].start <= byte_offset) {
if (spans[index].end > byte_offset)
active.push_back(const_cast<Span *>(&spans[index]));
index++;
}
Highlight *best = nullptr;
int max_prio = -1;
for (auto *s : active) {
if (s->hl->priority > max_prio) {
max_prio = s->hl->priority;
best = s->hl;
}
}
return best;
}
void sync(uint32_t byte_offset) {
active.clear();
size_t left = 0, right = spans.size();
while (left < right) {
size_t mid = (left + right) / 2;
if (spans[mid].start <= byte_offset)
left = mid + 1;
else
right = mid;
}
index = left;
while (left > 0) {
left--;
if (spans[left].end > byte_offset)
active.push_back(const_cast<Span *>(&spans[left]));
else if (byte_offset - spans[left].end > 1000)
break;
}
}
};
struct Editor {
const char *filename; // Filename of the editor
Knot *root; // A rope
std::shared_mutex knot_mtx; // A mutex
std::shared_mutex span_mtx; // A mutex
Coord cursor; // position of the cursor
uint32_t cursor_preffered; // preffered visual column
Coord selection; // position of the selection
bool selection_active; // true if there is a selection
Coord position; // Position of the editor
Coord size; // Size of the editor
Coord scroll; // Position of the scroll
TSTree *tree; // Tree-sitter tree
TSParser *parser; // Tree-sitter parser
TSQuery *query; // Tree-sitter query
const TSLanguage *language; // Tree-sitter language
Queue<TSInputEdit> edit_queue; // Tree-sitter edit queue
std::vector<Highlight> query_map; // Tree-sitter query map
int *folded; // folded lines indexed by line number - cached form of
// folded_node
std::vector<Span> spans;
std::map<uint32_t, bool> folded_node; // maps content hash to fold state
// - built by tree-sitter
};
typedef struct TSLoad {
Editor *editor;
char *prev = nullptr;
} TSLoad;
Editor *new_editor(const char *filename, Coord position, Coord size);
void free_editor(Editor *editor);
void render_editor(Editor *editor);
void fold(Editor *editor, uint32_t start_line, uint32_t end_line);
void scroll_up(Editor *editor, uint32_t lines);
void scroll_down(Editor *editor, uint32_t lines);
void cursor_up(Editor *editor, uint32_t number);
void cursor_down(Editor *editor, uint32_t number);
void cursor_left(Editor *editor, uint32_t number);
void cursor_right(Editor *editor, uint32_t number);
void ensure_scroll(Editor *editor);

157
include/rope.h Normal file
View File

@@ -0,0 +1,157 @@
#ifndef ROPE_HPP
#define ROPE_HPP
#include <cstdint>
#include <vector>
#define MIN_CHUNK_SIZE 64 // 64 Bytes
#define MAX_CHUNK_SIZE 1024 * 8 // 8192 Bytes (8 KiB)
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define DEPTH(n) ((n) ? (n)->depth : 0)
#define PCRE2_CODE_UNIT_WIDTH 8
#define PCRE_WORKSPACE_SIZE 512
// Rope node definition
typedef struct Knot {
Knot *left;
Knot *right;
uint8_t depth;
uint32_t chunk_size;
uint32_t line_count;
uint32_t char_count;
char data[];
} Knot;
typedef struct LineIterator {
Knot *node;
uint8_t top;
uint32_t offset;
uint32_t line;
Knot *stack[64];
} LineIterator;
typedef struct LeafIterator {
Knot *node;
uint8_t top;
uint32_t offset;
Knot *stack[64];
} LeafIterator;
typedef struct ByteIterator {
LeafIterator *it;
uint32_t offset_l;
uint32_t offset_g;
uint32_t char_count;
char *data;
} ByteIterator;
// Rope operations
// Takes lengt of string to be converted
// to rope and returns a suitable chunk size
// but rope should work with any positive chunk size
uint32_t optimal_chunk_size(uint64_t length);
// Takes a string (no need for null termination) and returns a rope
// len is the length of the string, and chunk size is the size of each chunk
// load does not free or consume the string.
// and the str can be freed after load has been run.
Knot *load(char *str, uint32_t len, uint32_t chunk_size);
// Balances the rope and returns the root
// n is no longer valid / do not free
// As rope is balanced by other functions
// this is not to be used directly
Knot *balance(Knot *n);
// Concatenates two ropes and returns the joined root
// Balances the ropes too, if needed
// left and right are no longer valid / do not free
// ! left and right should have the same chunk size !
Knot *concat(Knot *left, Knot *right);
// Used to insert text into the rope
// node (the rope being inserted into) is no longer valid after call
// instead use return value as the new node
// offset is the position of the insertion relative to the start of the rope
// str is the string to be inserted (no need for null termination)
// len is the length of the string
Knot *insert(Knot *node, uint32_t offset, char *str, uint32_t len);
// Similar to insert but for deletion
// node (the rope being deleted from) is no longer valid after call
// instead use return value as the new node
// offset is the position of the deletion relative to the start of the rope
// len is the length of the deletion
Knot *erase(Knot *node, uint32_t offset, uint32_t len);
// Used to read a string from the rope
// root is the rope to be read from
// offset is the position of the read relative to the start of the rope
// len is the length of the read
// returns a null terminated string, should be freed by the caller
char *read(Knot *root, uint32_t offset, uint32_t len);
// Used to split the rope into left and right ropes
// node is the rope to be split (it is no longer valid after call / do not free)
// offset is the position of the split relative to the start of the rope
// left and right are pointers set to the root of that side of the split
void split(Knot *node, uint32_t offset, Knot **left, Knot **right);
// Used to convert a byte offset to a line number that contains that byte
uint32_t byte_to_line(Knot *node, uint32_t offset);
// Used to convert a line number to a byte offset (start of the line)
// also sets out_len to the length of the line
uint32_t line_to_byte(Knot *node, uint32_t line, uint32_t *out_len);
// Used to start a line iterator from the start_line number
// root is the root of the rope
// returned iterator must be freed after iteration is done
LineIterator *begin_l_iter(Knot *root, uint32_t start_line);
// Each subsequent call returns the next line as a null terminated string
// `it` is the iterator returned from begin_l_iter
// After getting the necessary lines free the iterator (no need to go upto the
// end) returns null if there are no more lines All return strings `must` be
// freed by the caller
char *next_line(LineIterator *it);
// Used to start an iterator over leaf data
// root is the root of the rope
// the caller must free the iterator after use
LeafIterator *begin_k_iter(Knot *root);
// Returns the next leaf data as a null terminated string
// `it` is the iterator returned from begin_k_iter
// ! Strings returned must never be freed by the caller !
// to mutate the string a copy must be made
char *next_leaf(LeafIterator *it);
// Used to start an iterator over byte data (one byte at a time)
// Uses leaf iterator internally
// root is the root of the rope, the caller must free the iterator after use
ByteIterator *begin_b_iter(Knot *root);
// Returns the next byte from the iterator
// Returns '\0' if there are no more bytes left
// `it` is the iterator returned from begin_b_iter
char next_byte(ByteIterator *it);
// Used to search for a pattern in the rope
// Pattern is a null terminated string representing a regular expression (DFA
// compliant) I.e some forms of backtracking etc. are not supported
// root is the root of the rope to be searched
// Returns a vector of pairs of start and length offsets (in bytes)
std::vector<std::pair<size_t, size_t>> search_rope(Knot *root,
const char *pattern);
// Helper function to free the rope
// root is the root of the rope
// the root is no longer valid after call
// This must be called only once when the rope is no longer needed
void free_rope(Knot *root);
#endif // ROPE_HPP

7
include/ts.h Normal file
View File

@@ -0,0 +1,7 @@
#pragma once
#include "./editor.h"
#define HEX(s) (static_cast<uint32_t>(std::stoul(s, nullptr, 16)))
TSQuery *load_query(const char *query_path, Editor *editor);
void ts_collect_spans(Editor *editor);

View File

@@ -1,6 +1,8 @@
#pragma once
#include <atomic>
#include <cstdint>
#include <mutex>
#include <string.h>
#include <string>
#include <sys/ioctl.h>
#include <termios.h>
@@ -41,6 +43,10 @@
#define CNTRL_ALT 3
#define SHIFT 4
const char VS16_BYTE_A = '\xEF';
const char VS16_BYTE_B = '\xB8';
const char VS16_BYTE_C = '\x8F';
enum CellFlags : uint8_t {
CF_NONE = 0,
CF_ITALIC = 1 << 0,
@@ -49,12 +55,17 @@ enum CellFlags : uint8_t {
};
struct ScreenCell {
std::string utf8; // empty => no content
std::string utf8 = std::string(""); // empty => no content
uint32_t fg = 0;
uint32_t bg = 0;
uint8_t flags = CF_NONE;
};
struct Coord {
uint32_t row;
uint32_t col;
};
struct KeyEvent {
uint8_t key_type;
@@ -71,32 +82,25 @@ struct KeyEvent {
uint8_t mouse_modifier;
};
extern int rows, cols;
extern uint32_t rows, cols;
extern std::vector<ScreenCell> screen; // size rows*cols
extern std::vector<ScreenCell> old_screen;
extern std::mutex screen_mutex;
extern std::atomic<bool> running;
struct coords {
int row;
int col;
};
extern "C" {
void get_terminal_size();
void die(const char *s);
void enable_raw_mode();
void disable_raw_mode();
void start_screen();
Coord start_screen();
void end_screen();
void update(int row, int col, const char *utf8, uint32_t fg, uint32_t bg,
uint8_t flags);
void set_cursor(int row, int col, bool show_cursor_param);
void update(uint32_t row, uint32_t col, const char *utf8, uint32_t fg,
uint32_t bg, uint8_t flags);
void set_cursor(int row, int col, int show_cursor_param);
void render();
coords get_size();
int real_width(std::string str);
Coord get_size();
int read_input(char *buf, size_t buflen);
KeyEvent read_key_nonblock();
KeyEvent read_key();
}
int display_width(const char *str);

32
include/utils.h Normal file
View File

@@ -0,0 +1,32 @@
#include <mutex>
#include <queue>
#include <string>
template <typename T> struct Queue {
std::queue<T> q;
std::mutex m;
void push(T val) {
std::lock_guard<std::mutex> lock(m);
q.push(val);
}
bool pop(T &val) {
std::lock_guard<std::mutex> lock(m);
if (q.empty())
return false;
val = q.front();
q.pop();
return true;
}
bool empty() {
std::lock_guard<std::mutex> lock(m);
return q.empty();
}
};
uint32_t grapheme_strlen(const char *s);
uint32_t get_visual_col_from_bytes(const char *line, uint32_t byte_limit);
uint32_t get_bytes_from_visual_col(const char *line,
uint32_t target_visual_col);
void log(const char *fmt, ...);
std::string get_exe_dir();

View File

508
src/editor.cc Normal file
View File

@@ -0,0 +1,508 @@
extern "C" {
#include "../libs/libgrapheme/grapheme.h"
}
#include "../include/editor.h"
#include "../include/ts.h"
#include "../libs/tree-sitter-ruby/bindings/c/tree-sitter-ruby.h"
#include <cstdint>
#include <fstream>
char *load_file(const char *path, uint32_t *out_len) {
std::ifstream file(path, std::ios::in | std::ios::binary | std::ios::ate);
if (!file.is_open())
return nullptr;
std::streamsize len = file.tellg();
if (len < 0 || (std::uint32_t)len > 0xFFFFFFFF)
return nullptr;
file.seekg(0, std::ios::beg);
char *buf = (char *)malloc(static_cast<std::uint32_t>(len));
if (!buf)
return nullptr;
if (file.read(buf, len)) {
*out_len = static_cast<uint32_t>(len);
return buf;
} else {
free(buf);
return nullptr;
}
}
Editor *new_editor(const char *filename, Coord position, Coord size) {
Editor *editor = new Editor();
if (!editor)
return nullptr;
uint32_t len = 0;
char *str = load_file(filename, &len);
if (!str)
return nullptr;
editor->filename = filename;
editor->position = position;
editor->size = size;
editor->tree = nullptr;
editor->cursor = {0, 0};
editor->selection_active = false;
editor->selection = {0, 0};
editor->scroll = {0, 0};
editor->root = load(str, len, optimal_chunk_size(len));
editor->folded = (int *)calloc(editor->root->line_count + 2, sizeof(int));
std::string query = get_exe_dir() + "/../grammar/ruby.scm";
if (!(len > (1024 * 1024))) {
editor->parser = ts_parser_new();
editor->language = tree_sitter_ruby();
ts_parser_set_language(editor->parser, editor->language);
editor->query = load_query(query.c_str(), editor);
}
free(str);
return editor;
}
void free_editor(Editor *editor) {
if (editor->folded)
free(editor->folded);
ts_parser_delete(editor->parser);
if (editor->tree)
ts_tree_delete(editor->tree);
if (editor->query)
ts_query_delete(editor->query);
free_rope(editor->root);
delete editor;
}
void scroll_up(Editor *editor, uint32_t number) {
if (!editor || !editor->root || number == 0)
return;
uint32_t count = 0;
uint32_t visible_lines_checked = 0;
int32_t current_check_row = editor->scroll.row;
while (visible_lines_checked < number + 1 && current_check_row >= 0) {
if (editor->folded[current_check_row] != 1)
visible_lines_checked++;
count++;
current_check_row--;
}
if (current_check_row < 0)
count = editor->scroll.row;
LineIterator *it = begin_l_iter(editor->root, editor->scroll.row - count + 1);
std::vector<std::pair<uint32_t, uint32_t>> stack;
stack.reserve(count);
uint32_t lines_iterated = 0;
uint32_t start_row = editor->scroll.row - count + 1;
while (lines_iterated < count) {
char *line_content = next_line(it);
uint32_t current_idx = start_row + lines_iterated;
int fold_state = editor->folded[current_idx];
if (fold_state == 2) {
stack.push_back({1, current_idx});
} else if (fold_state == 0) {
uint32_t len =
(line_content != nullptr) ? grapheme_strlen(line_content) : 0;
stack.push_back({len, current_idx});
}
if (line_content)
free(line_content);
lines_iterated++;
}
uint32_t ln = 0;
uint32_t wrap_limit = editor->size.col;
for (int i = stack.size() - 1; i >= 0; i--) {
uint32_t len = stack[i].first;
uint32_t row_idx = stack[i].second;
uint32_t segments =
(wrap_limit > 0 && len > 0) ? (len + wrap_limit - 1) / wrap_limit : 1;
if (len == 0)
segments = 1;
for (int seg = (row_idx == editor->scroll.row)
? editor->scroll.col / wrap_limit
: segments - 1;
seg >= 0; seg--) {
ln++;
if (ln == number + 1) {
editor->scroll.row = row_idx;
editor->scroll.col = seg * wrap_limit;
free(it);
return;
}
}
}
if (ln < number + 1) {
editor->scroll.row = 0;
editor->scroll.col = 0;
}
free(it);
}
void scroll_down(Editor *editor, uint32_t number) {
if (!editor || !editor->root || number == 0)
return;
LineIterator *it = begin_l_iter(editor->root, editor->scroll.row);
uint32_t current_row = editor->scroll.row;
uint32_t lines_moved = 0;
uint32_t wrap_limit = editor->size.col;
if (wrap_limit == 0)
wrap_limit = 1;
while (lines_moved < number) {
char *line_content = next_line(it);
if (line_content == nullptr)
break;
int fold_state = editor->folded[current_row];
if (fold_state == 1) {
free(line_content);
current_row++;
continue;
}
uint32_t segments = 1;
if (fold_state == 2) {
segments = 1;
} else {
uint32_t len = grapheme_strlen(line_content);
segments = (len > 0) ? (len + wrap_limit - 1) / wrap_limit : 1;
}
uint32_t start_seg = (current_row == editor->scroll.row)
? (editor->scroll.col / wrap_limit)
: 0;
for (uint32_t seg = start_seg; seg < segments; seg++) {
if (current_row == editor->scroll.row && seg == start_seg)
continue;
lines_moved++;
if (lines_moved == number) {
editor->scroll.row = current_row;
editor->scroll.col = seg * wrap_limit;
free(line_content);
free(it);
return;
}
}
free(line_content);
current_row++;
}
free(it);
}
void cursor_down(Editor *editor, uint32_t number) {
if (!editor || !editor->root || number == 0)
return;
LineIterator *it = begin_l_iter(editor->root, editor->cursor.row);
char *line_content = next_line(it);
if (line_content == nullptr)
return;
uint32_t visual_col =
get_visual_col_from_bytes(line_content, editor->cursor.col);
do {
free(line_content);
line_content = next_line(it);
editor->cursor.row += 1;
if (editor->cursor.row >= editor->root->line_count) {
editor->cursor.row = editor->root->line_count - 1;
break;
};
if (editor->folded && editor->folded[editor->cursor.row] != 0)
number++;
} while (--number > 0);
free(it);
if (line_content == nullptr)
return;
editor->cursor.col = get_bytes_from_visual_col(line_content, visual_col);
free(line_content);
}
void cursor_up(Editor *editor, uint32_t number) {
if (!editor || !editor->root || number == 0)
return;
LineIterator *it = begin_l_iter(editor->root, editor->cursor.row);
char *line_content = next_line(it);
if (!line_content) {
free(it);
return;
}
uint32_t visual_col =
get_visual_col_from_bytes(line_content, editor->cursor.col);
free(line_content);
while (number > 0 && editor->cursor.row > 0) {
editor->cursor.row--;
if (editor->folded && editor->folded[editor->cursor.row] != 0)
continue;
number--;
}
free(it);
it = begin_l_iter(editor->root, editor->cursor.row);
line_content = next_line(it);
if (!line_content) {
free(it);
return;
}
editor->cursor.col = get_bytes_from_visual_col(line_content, visual_col);
free(line_content);
free(it);
}
void cursor_right(Editor *editor, uint32_t number) {
if (!editor || !editor->root || number == 0)
return;
LineIterator *it = begin_l_iter(editor->root, editor->cursor.row);
char *line = next_line(it);
free(it);
if (!line)
return;
uint32_t line_len = strlen(line);
if (line[line_len - 1] == '\n')
line[--line_len] = '\0';
while (number > 0) {
if (editor->cursor.col >= line_len) {
free(line);
line = nullptr;
uint32_t next_row = editor->cursor.row + 1;
while (editor->folded && next_row < editor->root->line_count &&
editor->folded[next_row] != 0)
next_row++;
if (next_row >= editor->root->line_count) {
editor->cursor.col = line_len;
break;
}
editor->cursor.row = next_row;
editor->cursor.col = 0;
it = begin_l_iter(editor->root, editor->cursor.row);
line = next_line(it);
free(it);
if (!line)
break;
line_len = strlen(line);
if (line[line_len - 1] == '\n')
line[--line_len] = '\0';
} else {
uint32_t inc = grapheme_next_character_break_utf8(
line + editor->cursor.col, line_len - editor->cursor.col);
if (inc == 0)
break;
editor->cursor.col += inc;
}
number--;
}
if (line)
free(line);
}
void cursor_left(Editor *editor, uint32_t number) {
if (!editor || !editor->root || number == 0)
return;
LineIterator *it = begin_l_iter(editor->root, editor->cursor.row);
char *line = next_line(it);
free(it);
if (!line)
return;
uint32_t len = strlen(line);
if (line[len - 1] == '\n')
line[--len] = '\0';
while (number > 0) {
if (editor->cursor.col == 0) {
free(line);
line = nullptr;
if (editor->cursor.row == 0)
break;
int32_t prev_row = editor->cursor.row - 1;
while (editor->folded && prev_row >= 0 && editor->folded[prev_row] != 0)
prev_row--;
if (prev_row < 0)
break;
editor->cursor.row = prev_row;
it = begin_l_iter(editor->root, editor->cursor.row);
line = next_line(it);
free(it);
if (!line)
break;
uint32_t len = strlen(line);
if (line[len - 1] == '\n')
line[--len] = '\0';
editor->cursor.col = len;
} else {
uint32_t col = editor->cursor.col;
uint32_t new_col = 0;
uint32_t visual_col = 0;
uint32_t len = strlen(line);
while (new_col < col) {
uint32_t inc =
grapheme_next_character_break_utf8(line + new_col, len - new_col);
if (new_col + inc >= col)
break;
new_col += inc;
visual_col++;
}
editor->cursor.col = new_col;
}
number--;
}
if (line)
free(line);
}
void ensure_scroll(Editor *editor) {
LineIterator *it = begin_l_iter(editor->root, editor->scroll.row);
uint32_t rendered_lines = 0;
uint32_t line_index = editor->scroll.row;
char *line_content = nullptr;
while (rendered_lines <= editor->size.row) {
line_content = next_line(it);
if (!line_content)
break;
char *line_content_t = line_content;
if (rendered_lines == 0)
line_content_t = line_content + editor->scroll.col;
uint32_t len = grapheme_strlen(line_content_t);
free(line_content);
uint32_t wrapped_lines = (len + editor->size.col - 1) / editor->size.col;
rendered_lines += wrapped_lines;
line_index++;
}
line_index -= 2;
free(it);
if (editor->cursor.row >= line_index && line_content)
scroll_down(editor, editor->cursor.row - line_index);
if (editor->cursor.row < editor->scroll.row)
scroll_up(editor, editor->scroll.row - editor->cursor.row);
}
void fold(Editor *editor, uint32_t start_line, uint32_t end_line) {
if (!editor)
return;
if (!editor->folded) {
editor->folded = (int *)calloc(editor->root->line_count + 2, sizeof(int));
if (!editor->folded)
return;
}
for (uint32_t i = start_line; i <= end_line && i < editor->size.row; i++)
editor->folded[i] = 1;
editor->folded[start_line] = 2;
}
void update_render_fold_marker(uint32_t row, uint32_t cols) {
const char *marker = "... folded ...";
uint32_t len = strlen(marker);
uint32_t i = 0;
for (; i < len && i < cols; i++)
update(row, i, (char[2]){marker[i], 0}, 0xc6c6c6, 0, 0);
for (; i < cols; i++)
update(row, i, " ", 0xc6c6c6, 0, 0);
}
void render_editor(Editor *editor) {
uint32_t screen_rows = editor->size.row;
uint32_t screen_cols = editor->size.col;
uint32_t line_index = editor->scroll.row;
SpanCursor span_cursor(editor->spans);
std::shared_lock knot_lock(editor->knot_mtx);
std::shared_lock span_lock(editor->span_mtx);
LineIterator *it = begin_l_iter(editor->root, line_index);
if (!it)
return;
uint32_t rendered_rows = 0;
uint32_t global_byte_offset = line_to_byte(editor->root, line_index, nullptr);
span_cursor.sync(global_byte_offset);
while (rendered_rows < screen_rows) {
if (editor->folded && editor->folded[line_index]) {
if (editor->folded[line_index] == 2) {
update_render_fold_marker(rendered_rows, screen_cols);
rendered_rows++;
}
do {
char *line = next_line(it);
if (!line)
break;
global_byte_offset += strlen(line);
if (line[strlen(line) - 1] == '\n')
global_byte_offset--;
global_byte_offset++;
free(line);
line_index++;
} while (line_index < editor->size.row &&
editor->folded[line_index] == 1);
continue;
}
char *line = next_line(it);
if (!line)
break;
uint32_t line_len = strlen(line);
if (line_len > 0 && line[line_len - 1] == '\n')
line_len--;
uint32_t current_byte_offset = 0;
if (rendered_rows == 0 && editor->scroll.col > 0) {
uint32_t skipped_cols = 0;
while (skipped_cols < editor->scroll.col &&
current_byte_offset < line_len) {
uint32_t len = grapheme_next_character_break_utf8(
line + current_byte_offset, line_len - current_byte_offset);
current_byte_offset += len;
skipped_cols++;
}
}
while (current_byte_offset < line_len && rendered_rows < screen_rows) {
uint32_t slice_byte_len = 0;
uint32_t slice_visual_cols = 0;
uint32_t probe_offset = current_byte_offset;
while (slice_visual_cols < screen_cols && probe_offset < line_len) {
uint32_t len = grapheme_next_character_break_utf8(
line + probe_offset, line_len - probe_offset);
slice_byte_len += len;
probe_offset += len;
slice_visual_cols++;
}
uint32_t col = 0;
uint32_t local_render_offset = 0;
while (local_render_offset < slice_byte_len) {
if (line_index == editor->cursor.row &&
editor->cursor.col == (current_byte_offset + local_render_offset))
set_cursor(editor->position.row + rendered_rows,
editor->position.col + col, 1);
uint32_t absolute_byte_pos =
global_byte_offset + current_byte_offset + local_render_offset;
Highlight *hl = span_cursor.get_highlight(absolute_byte_pos);
uint32_t fg = hl ? hl->fg : 0xFFFFFF;
uint32_t bg = hl ? hl->bg : 0;
uint8_t fl = hl ? hl->flags : 0;
uint32_t cluster_len = grapheme_next_character_break_utf8(
line + current_byte_offset + local_render_offset,
slice_byte_len - local_render_offset);
if (cluster_len == 0)
cluster_len = 1;
std::string cluster(line + current_byte_offset + local_render_offset,
cluster_len);
update(editor->position.row + rendered_rows, editor->position.col + col,
cluster.c_str(), fg, bg, fl);
local_render_offset += cluster_len;
col++;
}
if (line_index == editor->cursor.row &&
editor->cursor.col == (current_byte_offset + slice_byte_len))
set_cursor(editor->position.row + rendered_rows,
editor->position.col + col, 1);
while (col < screen_cols) {
update(editor->position.row + rendered_rows, editor->position.col + col,
" ", 0xFFFFFF, 0, 0);
col++;
}
rendered_rows++;
current_byte_offset += slice_byte_len;
}
if (line_len == 0 ||
(current_byte_offset >= line_len && rendered_rows == 0)) {
if (editor->cursor.row == line_index)
set_cursor(editor->position.row + rendered_rows, editor->position.col,
1);
uint32_t col = 0;
while (col < screen_cols) {
update(editor->position.row + rendered_rows, editor->position.col + col,
" ", 0xFFFFFF, 0, 0);
col++;
}
rendered_rows++;
}
global_byte_offset += line_len + 1;
line_index++;
free(line);
}
while (rendered_rows < screen_rows) {
for (uint32_t col = 0; col < screen_cols; col++)
update(editor->position.row + rendered_rows, editor->position.col + col,
" ", 0xFFFFFF, 0, 0);
rendered_rows++;
}
free(it);
}

View File

@@ -1,4 +1,4 @@
#include "../headers/header.hpp"
#include "../include/ui.h"
int read_input(char *buf, size_t buflen) {
size_t i = 0;
@@ -115,11 +115,9 @@ KeyEvent read_key_nonblock() {
}
KeyEvent read_key() {
KeyEvent ret;
while (1) {
ret = read_key_nonblock();
while (true) {
KeyEvent ret = read_key_nonblock();
if (ret.key_type != KEY_NONE)
return ret;
usleep(2500);
}
}

199
src/main.cc Normal file
View File

@@ -0,0 +1,199 @@
#include "../include/editor.h"
#include "../include/ts.h"
#include "../include/ui.h"
#include "../libs/tree-sitter/lib/include/tree_sitter/api.h"
#include <atomic>
#include <chrono>
#include <cstdint>
#include <iostream>
#include <sys/ioctl.h>
#include <thread>
std::atomic<bool> running{true};
Queue<KeyEvent> event_queue;
std::atomic<uint64_t> render_frames{0};
std::atomic<uint64_t> worker_frames{0};
auto start_time = std::chrono::high_resolution_clock::now();
void background_worker(Editor *editor) {
while (running) {
worker_frames++;
ts_collect_spans(editor);
std::this_thread::sleep_for(std::chrono::milliseconds(16));
}
}
void input_listener() {
while (running) {
KeyEvent event = read_key();
if (event.key_type == KEY_CHAR && event.c == CTRL('q'))
running = false;
event_queue.push(event);
}
}
void handle_editor_event(Editor *editor, KeyEvent event) {
if (event.key_type == KEY_SPECIAL && event.special_key == KEY_DOWN)
cursor_down(editor, 1);
if (event.key_type == KEY_SPECIAL && event.special_key == KEY_UP)
cursor_up(editor, 1);
if (event.key_type == KEY_SPECIAL && event.special_key == KEY_LEFT)
cursor_left(editor, 1);
if (event.key_type == KEY_SPECIAL && event.special_key == KEY_RIGHT)
cursor_right(editor, 1);
if (event.key_type == KEY_CHAR &&
((event.c >= 'a' && event.c <= 'z') ||
(event.c >= 'A' && event.c <= 'Z') ||
(event.c >= '0' && event.c <= '9') || event.c == ' ' || event.c == '!' ||
event.c == '@' || event.c == '#' || event.c == '$' || event.c == '%' ||
event.c == '^' || event.c == '&' || event.c == '*' || event.c == '(' ||
event.c == ')' || event.c == '-' || event.c == '_' || event.c == '=' ||
event.c == '+' || event.c == '[' || event.c == ']' || event.c == '{' ||
event.c == '}' || event.c == '\\' || event.c == '|' || event.c == ';' ||
event.c == ':' || event.c == '\'' || event.c == '"' || event.c == ',' ||
event.c == '.' || event.c == '<' || event.c == '>' || event.c == '/' ||
event.c == '?' || event.c == '`' || event.c == '~')) {
std::shared_lock lock_1(editor->knot_mtx);
uint32_t pos = line_to_byte(editor->root, editor->cursor.row, nullptr) +
editor->cursor.col;
lock_1.unlock();
std::unique_lock lock_2(editor->knot_mtx);
editor->root = insert(editor->root, pos, &event.c, 1);
lock_2.unlock();
if (editor->tree) {
TSInputEdit edit = {
.start_byte = pos,
.old_end_byte = pos,
.new_end_byte = pos + 1,
.start_point = {editor->cursor.row, editor->cursor.col},
.old_end_point = {editor->cursor.row, editor->cursor.col},
.new_end_point = {editor->cursor.row, editor->cursor.col + 1},
};
editor->edit_queue.push(edit);
}
cursor_right(editor, 1);
}
if (event.key_type == KEY_CHAR && event.c == '\t') {
std::shared_lock lock_1(editor->knot_mtx);
uint32_t pos = line_to_byte(editor->root, editor->cursor.row, nullptr) +
editor->cursor.col;
lock_1.unlock();
std::unique_lock lock_2(editor->knot_mtx);
editor->root = insert(editor->root, pos, (char *)" ", 2);
lock_2.unlock();
if (editor->tree) {
TSInputEdit edit = {
.start_byte = pos,
.old_end_byte = pos,
.new_end_byte = pos + 2,
.start_point = {editor->cursor.row, editor->cursor.col},
.old_end_point = {editor->cursor.row, editor->cursor.col},
.new_end_point = {editor->cursor.row, editor->cursor.col + 2},
};
editor->edit_queue.push(edit);
}
cursor_right(editor, 2);
}
if (event.key_type == KEY_CHAR && (event.c == '\n' || event.c == '\r')) {
std::shared_lock lock_1(editor->knot_mtx);
uint32_t pos = line_to_byte(editor->root, editor->cursor.row, nullptr) +
editor->cursor.col;
lock_1.unlock();
std::unique_lock lock_2(editor->knot_mtx);
editor->root = insert(editor->root, pos, (char *)"\n", 1);
lock_2.unlock();
if (editor->tree) {
TSInputEdit edit = {
.start_byte = pos,
.old_end_byte = pos,
.new_end_byte = pos + 1,
.start_point = {editor->cursor.row, editor->cursor.col},
.old_end_point = {editor->cursor.row, editor->cursor.col},
.new_end_point = {editor->cursor.row, editor->cursor.col + 1},
};
editor->edit_queue.push(edit);
}
cursor_right(editor, 1);
}
if (event.key_type == KEY_CHAR && event.c == 0x7F) {
std::shared_lock lock_1(editor->knot_mtx);
uint32_t pos = line_to_byte(editor->root, editor->cursor.row, nullptr) +
editor->cursor.col;
TSPoint old_point = {editor->cursor.row, editor->cursor.col};
cursor_left(editor, 1);
uint32_t start = line_to_byte(editor->root, editor->cursor.row, nullptr) +
editor->cursor.col;
lock_1.unlock();
std::unique_lock lock_2(editor->knot_mtx);
editor->root = erase(editor->root, start, pos - start);
lock_2.unlock();
if (editor->tree) {
TSInputEdit edit = {
.start_byte = start,
.old_end_byte = pos,
.new_end_byte = start,
.start_point = {editor->cursor.row, editor->cursor.col},
.old_end_point = old_point,
.new_end_point = {editor->cursor.row, editor->cursor.col},
};
editor->edit_queue.push(edit);
}
}
ensure_scroll(editor);
}
int main(int argc, char *argv[]) {
Coord screen = start_screen();
const char *filename = (argc > 1) ? argv[1] : "ts.cpp";
Editor *editor = new_editor(filename, {0, 0}, {screen.row, screen.col});
if (!editor) {
end_screen();
fprintf(stderr, "Failed to load editor\n");
return 1;
}
start_time = std::chrono::high_resolution_clock::now();
std::thread input_thread(input_listener);
std::thread work_thread(background_worker, editor);
while (running) {
render_frames++;
KeyEvent event;
while (event_queue.pop(event))
handle_editor_event(editor, event);
render_editor(editor);
render();
std::this_thread::sleep_for(std::chrono::milliseconds(16));
}
input_thread.detach();
if (work_thread.joinable())
work_thread.join();
auto end_time = std::chrono::high_resolution_clock::now();
double seconds = std::chrono::duration<double>(end_time - start_time).count();
double render_fps = render_frames / seconds;
double worker_fps = worker_frames / seconds;
end_screen();
std::cout << "\n======= Performance Summary =======\n";
std::cout << "Runtime: " << seconds << "s\n";
std::cout << "Render loop FPS: " << render_fps << "Hz\n";
std::cout << "Worker loop FPS: " << worker_fps << "Hz\n";
std::cout << "===================================\n";
free_editor(editor);
return 0;
}

View File

@@ -1,26 +1,29 @@
// includes
#include "../../libs/libgrapheme/grapheme.h"
#include "../../libs/unicode_width/unicode_width.h"
#include "../headers/header.hpp"
extern "C" {
#include "../libs/libgrapheme/grapheme.h"
#include "../libs/unicode_width/unicode_width.h"
}
#include "../include/ui.h"
struct termios orig_termios;
termios orig_termios;
int rows, cols;
bool show_cursor = false;
uint32_t rows, cols;
int show_cursor = 0;
std::vector<ScreenCell> screen;
std::vector<ScreenCell> old_screen;
std::mutex screen_mutex;
int real_width(std::string str) {
if (!str.size())
int display_width(const char *str) {
if (!str)
return 0;
if (!strlen(str))
return 0;
const char *p = str.c_str();
if (str[0] == '\t')
return 4;
unicode_width_state_t state;
unicode_width_init(&state);
int width = 0;
for (size_t j = 0; j < str.size(); j++) {
for (size_t j = 0; j < strlen(str); j++) {
unsigned char c = str[j];
if (c < 128) {
int char_width = unicode_width_process(&state, c);
@@ -28,7 +31,7 @@ int real_width(std::string str) {
width += char_width;
} else {
uint_least32_t cp;
size_t bytes = grapheme_decode_utf8(p + j, str.size() - j, &cp);
size_t bytes = grapheme_decode_utf8(str + j, strlen(str) - j, &cp);
if (bytes > 1) {
int char_width = unicode_width_process(&state, cp);
if (char_width > 0)
@@ -37,6 +40,7 @@ int real_width(std::string str) {
}
}
}
return width;
}
@@ -63,6 +67,7 @@ void enable_raw_mode() {
raw.c_oflag &= ~(OPOST);
raw.c_cflag |= (CS8);
raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
raw.c_lflag |= ISIG;
raw.c_cc[VMIN] = 0;
raw.c_cc[VTIME] = 0;
@@ -74,7 +79,7 @@ void enable_raw_mode() {
}
void disable_raw_mode() {
std::string os = "\x1b[?1049l\x1b[?25h";
std::string os = "\x1b[?1049l\x1b[2 q\x1b[?1002l\x1b[?25h";
write(STDOUT_FILENO, os.c_str(), os.size());
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios) == -1) {
perror("tcsetattr");
@@ -82,30 +87,40 @@ void disable_raw_mode() {
}
}
void start_screen() {
Coord start_screen() {
enable_raw_mode();
get_terminal_size();
screen.assign(rows * cols, {}); // allocate & zero-init
old_screen.assign(rows * cols, {}); // allocate & zero-init
return {rows, cols};
}
void end_screen() { disable_raw_mode(); }
void update(int row, int col, const char *utf8, uint32_t fg, uint32_t bg,
uint8_t flags) {
if (row < 0 || row >= rows || col < 0 || col >= cols)
Coord get_size() { return {rows, cols}; }
void update(uint32_t row, uint32_t col, const char *utf8, uint32_t fg,
uint32_t bg, uint8_t flags) {
if (row >= rows || col >= cols)
return;
int idx = row * cols + col;
uint32_t idx = row * cols + col;
std::lock_guard<std::mutex> lock(screen_mutex);
screen[idx].utf8 = utf8 ? utf8 : ""; // nullptr => empty string
screen[idx].utf8 = utf8 ? utf8 : "";
screen[idx].fg = fg;
screen[idx].bg = bg;
screen[idx].flags = flags;
}
coords get_size() { return {rows, cols}; }
bool ends_with(const std::string &string_to_check) {
size_t len = string_to_check.size();
if (len < 3)
return false;
return (string_to_check[len - 3] == VS16_BYTE_A) &&
(string_to_check[len - 2] == VS16_BYTE_B) &&
(string_to_check[len - 1] == VS16_BYTE_C);
}
void render() {
static bool first_render = true;
@@ -114,147 +129,135 @@ void render() {
bool current_italic = false;
bool current_bold = false;
bool current_underline = false;
std::lock_guard<std::mutex> lock(screen_mutex);
std::string out;
// reserve a conservative amount to avoid repeated reallocs
out.reserve(static_cast<size_t>(rows) * static_cast<size_t>(cols) * 4 + 256);
// save cursor + hide
out += "\x1b[s\x1b[?25l";
if (first_render) {
out += "\x1b[2J\x1b[H";
first_render = false;
}
for (int row = 0; row < rows; ++row) {
for (uint32_t row = 0; row < rows; ++row) {
int first_change_col = -1;
int last_change_col = -1;
// detect change span in this row
for (int col = 0; col < cols; ++col) {
int idx = row * cols + col;
for (uint32_t col = 0; col < cols; ++col) {
uint32_t idx = row * cols + col;
ScreenCell &old_cell = old_screen[idx];
ScreenCell &new_cell = screen[idx];
bool old_empty = old_cell.utf8.empty();
bool new_empty = new_cell.utf8.empty();
bool content_changed =
(old_empty && !new_empty) || (!old_empty && new_empty) ||
(!old_empty && !new_empty && old_cell.utf8 != new_cell.utf8);
bool content_changed = old_cell.utf8 != new_cell.utf8;
bool style_changed =
(old_cell.fg != new_cell.fg) || (old_cell.bg != new_cell.bg) ||
((old_cell.flags & CF_ITALIC) != (new_cell.flags & CF_ITALIC)) ||
((old_cell.flags & CF_BOLD) != (new_cell.flags & CF_BOLD)) ||
((old_cell.flags & CF_UNDERLINE) != (new_cell.flags & CF_UNDERLINE));
if (content_changed || style_changed) {
if (first_change_col == -1)
first_change_col = col;
last_change_col = col;
}
}
if (first_change_col == -1)
continue;
// move cursor once to the start of change region
char buf[32];
int n = snprintf(buf, sizeof(buf), "\x1b[%d;%dH", row + 1,
first_change_col + 1);
out.append(buf, n);
// render changed region
char buf[64];
snprintf(buf, sizeof(buf), "\x1b[%d;%dH", row + 1, first_change_col + 1);
out.append(buf);
for (int col = first_change_col; col <= last_change_col; ++col) {
int idx = row * cols + col;
ScreenCell &old_cell = old_screen[idx];
ScreenCell &new_cell = screen[idx];
// foreground change
if (current_fg != new_cell.fg) {
if (new_cell.fg) {
char fb[64];
int m = snprintf(
fb, sizeof(fb), "\x1b[38;2;%d;%d;%dm", (new_cell.fg >> 16) & 0xFF,
(new_cell.fg >> 8) & 0xFF, (new_cell.fg >> 0) & 0xFF);
out.append(fb, m);
snprintf(fb, sizeof(fb), "\x1b[38;2;%d;%d;%dm",
(new_cell.fg >> 16) & 0xFF, (new_cell.fg >> 8) & 0xFF,
(new_cell.fg >> 0) & 0xFF);
out.append(fb);
} else {
out += "\x1b[39m";
}
current_fg = new_cell.fg;
}
// background change
if (current_bg != new_cell.bg) {
if (new_cell.bg) {
char bb[64];
int m = snprintf(
bb, sizeof(bb), "\x1b[48;2;%d;%d;%dm", (new_cell.bg >> 16) & 0xFF,
(new_cell.bg >> 8) & 0xFF, (new_cell.bg >> 0) & 0xFF);
out.append(bb, m);
snprintf(bb, sizeof(bb), "\x1b[48;2;%d;%d;%dm",
(new_cell.bg >> 16) & 0xFF, (new_cell.bg >> 8) & 0xFF,
(new_cell.bg >> 0) & 0xFF);
out.append(bb);
} else {
out += "\x1b[49m";
}
current_bg = new_cell.bg;
}
// italic
bool italic = (new_cell.flags & CF_ITALIC) != 0;
if (italic != current_italic) {
out += italic ? "\x1b[3m" : "\x1b[23m";
current_italic = italic;
}
// bold
bool bold = (new_cell.flags & CF_BOLD) != 0;
if (bold != current_bold) {
out += bold ? "\x1b[1m" : "\x1b[22m";
current_bold = bold;
}
// underline
bool underline = (new_cell.flags & CF_UNDERLINE) != 0;
if (underline != current_underline) {
out += underline ? "\x1b[4m" : "\x1b[24m";
current_underline = underline;
}
// content
if (!new_cell.utf8.empty()) {
if (new_cell.utf8[0] == '\t')
if (new_cell.utf8[0] == '\t') {
out.append(" ");
else
} else {
// HACK: This is a hack to work around the fact that emojis should be
// double width but handling them as so requires a lot of
// calculations for word wrapping so eventually have to do that
// and render them as the 2 wide they should be.
if (new_cell.utf8.size() > 1) {
if (new_cell.utf8.size() >= 3 && ends_with(new_cell.utf8)) {
out.append(new_cell.utf8.substr(0, new_cell.utf8.size() - 3));
out.append("\xEF\xB8\x8E");
} else {
out.append(new_cell.utf8);
out.append("\xEF\xB8\x8E");
}
} else {
out.append(new_cell.utf8);
}
}
} else {
out.append(1, ' ');
}
// copy new -> old (std::string assignment, no strdup/free)
old_cell.utf8 = new_cell.utf8;
old_cell.fg = new_cell.fg;
old_cell.bg = new_cell.bg;
old_cell.flags = new_cell.flags;
}
}
// final reset + restore cursor + show cursor
out += "\x1b[0m";
out += "\x1b[u";
if (show_cursor)
out += "\x1b[?25h";
// single syscall to write the whole frame
ssize_t written = write(STDOUT_FILENO, out.data(), out.size());
(void)written; // you may check for errors in debug builds
const char *ptr = out.data();
size_t remaining = out.size();
while (remaining > 0) {
ssize_t written = write(STDOUT_FILENO, ptr, remaining);
if (written == 0) {
break;
} else if (written == -1) {
if (errno == EINTR)
continue;
die("write failed");
break;
} else {
ptr += written;
remaining -= written;
}
}
}
void set_cursor(int row, int col, bool show_cursor_param) {
void set_cursor(int row, int col, int show_cursor_param) {
char buf[32];
int n = snprintf(buf, sizeof(buf), "\x1b[%d;%dH", row + 1, col + 1);
int n = snprintf(buf, sizeof(buf), "\x1b[%d;%dH\x1b[5 q", row + 1, col + 1);
show_cursor = show_cursor_param;
write(STDOUT_FILENO, buf, n);
}

858
src/rope.cc Normal file
View File

@@ -0,0 +1,858 @@
#include "../include/rope.h"
#include <assert.h>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <pcre2.h>
#include <stdio.h>
#include <string.h>
static void update(Knot *n) {
if (!n)
return;
if (!n->depth || n->depth == 0)
return;
uint32_t left_chars = n->left ? n->left->char_count : 0;
uint32_t right_chars = n->right ? n->right->char_count : 0;
n->char_count = left_chars + right_chars;
uint32_t left_lines = n->left ? n->left->line_count : 0;
uint32_t right_lines = n->right ? n->right->line_count : 0;
n->line_count = left_lines + right_lines;
uint8_t left_depth = n->left ? n->left->depth : 0;
uint8_t right_depth = n->right ? n->right->depth : 0;
n->depth = MAX(left_depth, right_depth) + 1;
n->chunk_size = n->left ? n->left->chunk_size : n->right->chunk_size;
}
// str is not consumed and \0 is not handled
// So if str is null terminated then len must be strlen(str)
// and freed by caller
Knot *load(char *str, uint32_t len, uint32_t chunk_size) {
if (len > (uint32_t)(chunk_size - (chunk_size / 16))) {
Knot *left = load(str, len / 2, chunk_size);
Knot *right = load(str + len / 2, len - len / 2, chunk_size);
Knot *node = (Knot *)malloc(sizeof(Knot));
if (!node)
return nullptr;
node->left = left;
node->right = right;
node->chunk_size = chunk_size;
node->depth = MAX(left->depth, right->depth) + 1;
node->char_count = left->char_count + right->char_count;
node->line_count = left->line_count + right->line_count;
return node;
} else {
Knot *node = (Knot *)malloc(sizeof(Knot) + chunk_size);
if (!node)
return nullptr;
node->left = nullptr;
node->right = nullptr;
node->chunk_size = chunk_size;
node->depth = 0;
node->char_count = len;
uint32_t newline_count = 0;
for (uint32_t i = 0; i < len; i++) {
char c = str[i];
node->data[i] = c;
if (c == '\n')
newline_count++;
}
node->line_count = newline_count;
return node;
}
}
// leaf if consumed and freed (so dont use or free it after)
// left and right are the new nodes
static void split_leaf(Knot *leaf, uint32_t k, Knot **left, Knot **right) {
Knot *left_node = (Knot *)malloc(sizeof(Knot) + leaf->chunk_size);
left_node->left = nullptr;
left_node->right = nullptr;
left_node->chunk_size = leaf->chunk_size;
left_node->depth = 0;
left_node->char_count = k;
uint32_t newline_count = 0;
for (uint32_t i = 0; i < k; i++) {
char c = leaf->data[i];
left_node->data[i] = c;
if (c == '\n')
newline_count++;
}
left_node->line_count = newline_count;
uint16_t right_line_count = leaf->line_count - newline_count;
*left = left_node;
Knot *right_node = (Knot *)malloc(sizeof(Knot) + leaf->chunk_size);
right_node->left = nullptr;
right_node->right = nullptr;
right_node->chunk_size = leaf->chunk_size;
right_node->depth = 0;
right_node->char_count = leaf->char_count - k;
right_node->line_count = right_line_count;
for (uint32_t i = k; i < leaf->char_count; i++) {
char c = leaf->data[i];
right_node->data[i - k] = c;
}
*right = right_node;
free(leaf);
}
// This makes node nonsensical, so dont use or free it after
void split(Knot *node, uint32_t offset, Knot **left, Knot **right) {
if (!node) {
*left = nullptr;
*right = nullptr;
return;
}
if (node->depth == 0) {
split_leaf(node, offset, left, right);
return;
}
uint32_t left_size = node->left ? node->left->char_count : 0;
if (offset < left_size) {
Knot *L = nullptr, *R = nullptr;
split(node->left, offset, &L, &R);
node->left = R;
update(node);
*right = node;
*left = L;
} else {
uint32_t new_offset = offset - left_size;
Knot *L = nullptr, *R = nullptr;
split(node->right, new_offset, &L, &R);
node->right = L;
update(node);
*left = node;
*right = R;
}
}
static inline int get_balance_factor(Knot *n) {
if (!n)
return 0;
return (int)DEPTH(n->left) - (int)DEPTH(n->right);
}
static inline Knot *rotate_right(Knot *y) {
Knot *x = y->left;
Knot *T2 = x->right;
x->right = y;
y->left = T2;
update(y);
update(x);
return x;
}
static inline Knot *rotate_left(Knot *x) {
Knot *y = x->right;
Knot *T2 = y->left;
y->left = x;
x->right = T2;
update(x);
update(y);
return y;
}
// Technically n can be used after calling
// but use return value instead
Knot *balance(Knot *n) {
update(n);
int bal = get_balance_factor(n);
if (bal > 1) {
if (get_balance_factor(n->left) < 0)
n->left = rotate_left(n->left);
return rotate_right(n);
}
if (bal < -1) {
if (get_balance_factor(n->right) > 0)
n->right = rotate_right(n->right);
return rotate_left(n);
}
return n;
}
// Dont free left or right after calling (only free return value)
// Assumes both ropes have equal chunk sizes
Knot *concat(Knot *left, Knot *right) {
if (!left)
return right;
if (!right)
return left;
if (!left || left->char_count == 0) {
if (left)
free_rope(left);
return right;
}
if (!right || right->char_count == 0) {
if (right)
free_rope(right);
return left;
}
if (left->depth == 0 && right->depth == 0) {
if (left->char_count + right->char_count <= left->chunk_size) {
Knot *node = (Knot *)malloc(sizeof(Knot) + left->chunk_size);
node->left = nullptr;
node->right = nullptr;
node->chunk_size = left->chunk_size;
node->depth = 0;
node->char_count = left->char_count + right->char_count;
node->line_count = left->line_count + right->line_count;
memcpy(node->data, left->data, left->char_count);
memcpy(node->data + left->char_count, right->data, right->char_count);
free(left);
free(right);
return node;
}
}
uint16_t d_left = left->depth;
uint16_t d_right = right->depth;
if (d_left > d_right + 1) {
left->right = concat(left->right, right);
return balance(left);
}
if (d_right > d_left + 1) {
right->left = concat(left, right->left);
return balance(right);
}
Knot *node = (Knot *)malloc(sizeof(Knot));
if (!node)
return nullptr;
node->left = left;
node->right = right;
node->chunk_size = left->chunk_size;
node->depth = MAX(d_left, d_right) + 1;
update(node);
return node;
}
// This makes node nonsensical, so dont use or free it after
// Instead, free the return value or use it in node's place
Knot *insert(Knot *node, uint32_t offset, char *str, uint32_t len) {
if (!node)
return nullptr;
if (len == 0)
return node;
if (node->depth == 0 && node->char_count + len <= node->chunk_size) {
if (offset < node->char_count)
memmove(node->data + offset + len, node->data + offset,
node->char_count - offset);
memcpy(node->data + offset, str, len);
node->char_count += len;
for (uint32_t i = 0; i < len; i++)
if (str[i] == '\n')
node->line_count++;
return node;
}
if (node->depth > 0) {
uint32_t left_count = node->left ? node->left->char_count : 0;
if (offset < left_count) {
Knot *new_left = insert(node->left, offset, str, len);
node->left = new_left;
update(node);
return balance(node);
} else {
Knot *new_right = insert(node->right, offset - left_count, str, len);
node->right = new_right;
update(node);
return balance(node);
}
}
Knot *left_part = nullptr;
Knot *right_part = nullptr;
split(node, offset, &left_part, &right_part);
Knot *middle_part = load(str, len, node->chunk_size);
return concat(concat(left_part, middle_part), right_part);
}
// This makes node nonsensical, so dont use or free it after
// Instead, free the return value or use it in node's place
Knot *erase(Knot *node, uint32_t offset, uint32_t len) {
if (!node || len == 0 || offset >= node->char_count)
return node;
if (offset + len > node->char_count)
len = node->char_count - offset;
if (node->depth == 0) {
uint32_t deleted_newlines = 0;
for (uint32_t i = offset; i < offset + len; i++)
if (node->data[i] == '\n')
deleted_newlines++;
node->line_count -= deleted_newlines;
if (offset + len < node->char_count)
memmove(node->data + offset, node->data + offset + len,
node->char_count - (offset + len));
node->char_count -= len;
return node;
}
uint32_t left_count = node->left ? node->left->char_count : 0;
if (offset + len <= left_count) {
node->left = erase(node->left, offset, len);
} else if (offset >= left_count) {
node->right = erase(node->right, offset - left_count, len);
} else {
Knot *left = nullptr, *middle = nullptr, *right = nullptr;
split(node, offset, &left, &right);
split(right, len, &middle, &right);
free_rope(middle);
return concat(left, right);
}
update(node);
return balance(node);
}
static void _read_into(Knot *node, uint32_t offset, uint32_t len, char *dest) {
if (!node || len == 0)
return;
if (node->depth == 0) {
memcpy(dest, node->data + offset, len);
return;
}
Knot *left = node->left;
uint32_t left_count = left ? left->char_count : 0;
if (offset < left_count) {
uint32_t chunk_len = left_count - offset;
if (chunk_len > len)
chunk_len = len;
_read_into(left, offset, chunk_len, dest);
dest += chunk_len;
len -= chunk_len;
offset = 0;
} else {
offset -= left_count;
}
if (len > 0 && node->right)
_read_into(node->right, offset, len, dest);
}
char *read(Knot *root, uint32_t offset, uint32_t len) {
if (!root)
return nullptr;
if (offset >= root->char_count) {
char *empty = (char *)malloc(1);
if (empty)
empty[0] = '\0';
return empty;
}
if (offset + len > root->char_count) {
len = root->char_count - offset;
}
char *buffer = (char *)malloc((len + 1) * sizeof(char));
if (!buffer)
return nullptr;
_read_into(root, offset, len, buffer);
buffer[len] = '\0';
return buffer;
}
// Hopefully free the tree only once at the end of its use using the pointer
// from the last insert or concat or erase call.
// (or use twice if last call was split - for both left and right).
void free_rope(Knot *root) {
if (!root)
return;
free_rope(root->left);
free_rope(root->right);
free(root);
}
static uint32_t find_nth_newline_offset(Knot *node, uint32_t n) {
if (!node || n > node->line_count)
return UINT32_MAX;
if (node->depth == 0) {
uint32_t count = 0;
for (uint32_t i = 0; i < node->char_count; i++) {
if (node->data[i] == '\n') {
if (count == n)
return i;
count++;
}
}
return UINT32_MAX;
}
uint32_t left_lines = node->left ? node->left->line_count : 0;
if (n < left_lines) {
return find_nth_newline_offset(node->left, n);
} else {
uint32_t right_offset =
find_nth_newline_offset(node->right, n - left_lines);
if (right_offset == UINT32_MAX)
return UINT32_MAX;
uint32_t left_chars = node->left ? node->left->char_count : 0;
return left_chars + right_offset;
}
}
uint32_t byte_to_line(Knot *node, uint32_t offset) {
if (!node)
return 0;
if (offset >= node->char_count)
return node->line_count;
if (node->depth == 0) {
uint32_t lines_before = 0;
uint32_t limit = (offset < node->char_count) ? offset : node->char_count;
for (uint32_t i = 0; i < limit; i++)
if (node->data[i] == '\n')
lines_before++;
return lines_before;
}
uint32_t left_chars = node->left ? node->left->char_count : 0;
if (offset < left_chars) {
return byte_to_line(node->left, offset);
} else {
uint32_t left_lines = node->left ? node->left->line_count : 0;
return left_lines + byte_to_line(node->right, offset - left_chars);
}
}
uint32_t line_to_byte(Knot *node, uint32_t line, uint32_t *out_len) {
if (!node) {
if (out_len)
*out_len = 0;
return 0;
}
uint32_t start_offset = 0;
uint32_t end_offset = 0;
if (line == 0) {
start_offset = 0;
} else {
uint32_t prev_newline = find_nth_newline_offset(node, line - 1);
if (prev_newline == UINT32_MAX)
start_offset = node->char_count;
else
start_offset = prev_newline + 1;
}
uint32_t current_newline = find_nth_newline_offset(node, line);
if (current_newline == UINT32_MAX)
end_offset = node->char_count;
else
end_offset = current_newline + 1;
if (out_len) {
if (end_offset > start_offset)
*out_len = end_offset - start_offset;
else
*out_len = 0;
}
return start_offset;
}
LineIterator *begin_l_iter(Knot *root, uint32_t start_line) {
if (!root)
return nullptr;
if (start_line > root->line_count)
return nullptr;
LineIterator *it = (LineIterator *)malloc(sizeof(LineIterator));
if (!it)
return nullptr;
it->top = 0;
it->line = start_line;
it->node = nullptr;
if (start_line == 0) {
it->offset = 0;
while (root->left) {
it->stack[it->top++] = root;
root = root->left;
}
if (!root->left && !root->right)
it->node = root;
it->stack[it->top++] = root;
return it;
}
Knot *curr = root;
uint32_t relative_line = --start_line;
it->line = start_line;
while (curr) {
it->stack[it->top++] = curr;
if (!curr->left && !curr->right) {
it->node = curr;
break;
}
uint32_t left_lines = (curr->left) ? curr->left->line_count : 0;
if (relative_line < left_lines) {
curr = curr->left;
} else {
relative_line -= left_lines;
curr = curr->right;
}
}
if (!it->node) {
free(it);
return nullptr;
}
it->offset = 0;
if (relative_line > 0) {
uint32_t found_newlines = 0;
uint32_t i = 0;
for (i = 0; i < it->node->char_count; i++) {
if (it->node->data[i] == '\n') {
found_newlines++;
if (found_newlines == relative_line) {
it->offset = i + 1;
break;
}
}
}
}
free(next_line(it));
return it;
}
static inline void iter_advance_leaf(LineIterator *it) {
if (it->top == 0) {
it->node = nullptr;
return;
}
Knot *prev = it->stack[--it->top];
while (it->top > 0) {
Knot *parent = it->stack[it->top - 1];
if (parent->left == prev && parent->right) {
Knot *curr = parent->right;
while (curr) {
it->stack[it->top++] = curr;
if (!curr->left && !curr->right) {
it->node = curr;
it->offset = 0;
return;
}
curr = (curr->left) ? curr->left : curr->right;
}
}
prev = it->stack[--it->top];
}
it->node = nullptr;
}
char *next_line(LineIterator *it) {
if (!it || !it->node)
return nullptr;
size_t capacity = 128;
size_t len = 0;
char *buffer = (char *)malloc(capacity);
if (!buffer)
return nullptr;
while (it->node) {
if (it->offset >= it->node->char_count) {
iter_advance_leaf(it);
if (!it->node)
break;
}
char *start = it->node->data + it->offset;
char *end = it->node->data + it->node->char_count;
char *newline_ptr = (char *)memchr(start, '\n', end - start);
size_t chunk_len;
bool found_newline = false;
if (newline_ptr) {
chunk_len = (newline_ptr - start) + 1;
found_newline = true;
} else {
chunk_len = end - start;
}
if (len + chunk_len + 1 > capacity) {
capacity = (capacity * 2) + chunk_len;
char *new_buf = (char *)realloc(buffer, capacity);
if (!new_buf) {
free(buffer);
return nullptr;
}
buffer = new_buf;
}
memcpy(buffer + len, start, chunk_len);
len += chunk_len;
it->offset += chunk_len;
if (found_newline) {
buffer[len] = '\0';
it->line++;
return buffer;
}
}
if (len > 0) {
buffer[len] = '\0';
it->line++;
return buffer;
}
free(buffer);
return nullptr;
}
LeafIterator *begin_k_iter(Knot *root) {
if (!root)
return nullptr;
LeafIterator *it = (LeafIterator *)malloc(sizeof(LeafIterator));
if (!it)
return nullptr;
it->top = 0;
Knot *curr = root;
while (curr) {
it->stack[it->top++] = curr;
if (!curr->left && !curr->right) {
it->node = curr;
return it;
}
curr = curr->left;
if (!curr) {
curr = it->stack[--it->top]->right;
Knot *temp = it->stack[it->top];
it->stack[it->top++] = temp;
curr = temp->left ? temp->left : temp->right;
Knot *parent = it->stack[it->top - 1];
curr = parent->left;
if (!curr) {
curr = parent->right;
}
}
}
free(it);
return nullptr;
}
// Caller must never free the returned string
char *next_leaf(LeafIterator *it) {
if (!it || !it->node)
return nullptr;
char *data_to_return = it->node->data;
data_to_return[it->node->char_count] = '\0';
Knot *prev_leaf = it->node;
Knot *parent = nullptr;
while (it->top > 0) {
parent = it->stack[--it->top];
if (parent->right && parent->right != prev_leaf) {
Knot *curr = parent->right;
while (curr) {
it->stack[it->top++] = curr;
if (!curr->left && !curr->right) {
it->node = curr;
return data_to_return;
}
curr = curr->left;
if (!curr)
curr = it->stack[it->top - 1]->right;
}
}
prev_leaf = parent;
}
it->node = nullptr;
return data_to_return;
}
ByteIterator *begin_b_iter(Knot *root) {
ByteIterator *b_it = (ByteIterator *)malloc(sizeof(ByteIterator));
LeafIterator *l_it = begin_k_iter(root);
b_it->it = l_it;
b_it->offset_g = 0;
b_it->offset_l = 0;
b_it->char_count = 0;
b_it->data = nullptr;
return b_it;
}
char next_byte(ByteIterator *it) {
if (it->data && it->offset_l < it->char_count) {
return it->data[it->offset_l++];
} else {
it->offset_g += it->offset_l;
it->offset_l = 1;
char *data = next_leaf(it->it);
it->char_count = strlen(data);
it->data = data;
if (it->data)
return *it->data;
else
return '\0';
}
}
std::vector<std::pair<size_t, size_t>> search_rope(Knot *root,
const char *pattern) {
std::vector<std::pair<size_t, size_t>> results;
int errorcode;
PCRE2_SIZE erroffset;
pcre2_code *re = pcre2_compile((PCRE2_SPTR)pattern, PCRE2_ZERO_TERMINATED, 0,
&errorcode, &erroffset, nullptr);
if (!re) {
fprintf(stderr, "PCRE2 compile error: %d\n", errorcode);
return results;
}
pcre2_match_data *mdata = pcre2_match_data_create(128, nullptr);
int workspace[PCRE_WORKSPACE_SIZE];
LeafIterator *it = begin_k_iter(root);
if (!it) {
pcre2_code_free(re);
pcre2_match_data_free(mdata);
return results;
}
size_t chunk_abs_offset = 0;
size_t saved_match_start = 0;
bool match_in_progress = false;
int flags = PCRE2_PARTIAL_SOFT;
while (1) {
const char *chunk_start = next_leaf(it);
if (!chunk_start)
break;
size_t chunk_len = strlen(chunk_start);
const char *current_ptr = chunk_start;
size_t remaining_len = chunk_len;
while (remaining_len > 0) {
int rc =
pcre2_dfa_match(re, (PCRE2_SPTR)current_ptr, remaining_len, 0, flags,
mdata, nullptr, workspace, PCRE_WORKSPACE_SIZE);
if (rc >= 0) {
PCRE2_SIZE *ov = pcre2_get_ovector_pointer(mdata);
size_t match_start_abs;
size_t match_end_abs;
if (match_in_progress) {
match_start_abs = saved_match_start;
match_end_abs =
chunk_abs_offset + (current_ptr - chunk_start) + ov[1];
} else {
match_start_abs =
chunk_abs_offset + (current_ptr - chunk_start) + ov[0];
match_end_abs =
chunk_abs_offset + (current_ptr - chunk_start) + ov[1];
}
size_t total_len = match_end_abs - match_start_abs;
results.push_back(std::make_pair(match_start_abs, total_len));
size_t consumed = ov[1];
if (consumed == 0)
consumed = 1;
current_ptr += consumed;
if (consumed > remaining_len)
remaining_len = 0;
else
remaining_len -= consumed;
match_in_progress = false;
flags = PCRE2_PARTIAL_SOFT | PCRE2_NOTBOL;
continue;
} else if (rc == PCRE2_ERROR_PARTIAL) {
PCRE2_SIZE *ov = pcre2_get_ovector_pointer(mdata);
if (!match_in_progress) {
saved_match_start =
chunk_abs_offset + (current_ptr - chunk_start) + ov[0];
match_in_progress = true;
}
flags |= PCRE2_DFA_RESTART;
flags |= PCRE2_NOTBOL;
break;
} else {
if (match_in_progress) {
match_in_progress = false;
flags = PCRE2_PARTIAL_SOFT | PCRE2_NOTBOL;
current_ptr++;
remaining_len--;
} else {
break;
}
// if (rc != PCRE2_ERROR_NOMATCH) {} // handle error
}
}
chunk_abs_offset += chunk_len;
if (!match_in_progress)
flags = PCRE2_PARTIAL_SOFT | PCRE2_NOTBOL;
}
pcre2_match_data_free(mdata);
pcre2_code_free(re);
free(it);
return results;
}
uint32_t optimal_chunk_size(uint64_t length) {
if (length <= MIN_CHUNK_SIZE)
return MIN_CHUNK_SIZE;
double target_exponent = MIN(std::log2((double)MAX_CHUNK_SIZE),
7.0 + (std::log2((double)length) - 10.0) * 0.25);
uint32_t final_chunk_size =
MAX((uint32_t)MIN_CHUNK_SIZE, (uint32_t)std::pow(2.0, target_exponent));
final_chunk_size = MIN(final_chunk_size, (uint32_t)MAX_CHUNK_SIZE);
final_chunk_size = 1U << (32 - __builtin_clz(final_chunk_size - 1));
return final_chunk_size;
}
// Basic correctness test & usage example
/*
int _main() {
char *buffer = (char *)malloc(44 * 4 + 5);
strcpy(buffer, "The quick brown fox jumps over the lazy dog.\n\
The quick brown fox jumps over the lazy dog.\n\
The quick brown fox jumps over the lazy dog.\n\
The quick brown fox jumps over the lazy dog.");
// This loads all (excluding \0 put in by strcpy)
Knot *root = load(buffer, 44 * 4 + 3, optimal_chunk_size(44 * 4 + 3));
Knot *left = nullptr, *right = nullptr;
// Splits root into left and right (root is no longer valid)
split(root, 5, &left, &right);
// simple read based on byte offset and length
char *s1 = read(left, 0, 100);
printf("%s\n:\n", s1);
char *s2 = read(right, 0, 100);
printf("%s\n;\n", s2);
free(s1);
free(s2);
// Recombines left and right into root (both can
// be valid or invalid in optimized cases)
// they are to not be used after concat
root = concat(left, right);
// root should be set to return value from insert always
root = insert(root, 5, buffer, 5);
free(buffer);
char *s3 = read(root, 0, 100);
printf("%s\n,\n", s3);
// Similar to insert but for erase
root = erase(root, 5, 5);
char *s4 = read(root, 0, 100);
printf("%s\n.\n", s4);
free(s3);
free(s4);
uint32_t byte_offset;
uint32_t len;
// Byte offset given reltive to how it would
// be in a file offset + len includes the \n
// at the end of the line (or nothing is EOF)
byte_offset = line_to_byte(root, 2, &len);
char *s5 = read(root, byte_offset, len);
printf("%s\n'\n", s5);
free(s5);
// returns line number of which line that
// byte position would be in.
// the ending \n position is included in this
uint32_t line = byte_to_line(root, byte_offset + len - 1);
printf("%u\n:\n", line);
// From second line onwards (0 indexed)
LineIterator *it = begin_l_iter(root, 0);
char *c = nullptr;
while ((c = next_line(it)) != nullptr) {
printf("%s :wow:\n", c);
free(c);
}
free(it);
printf("\n/\n");
// Starts at first byte (to be used for regex search)
ByteIterator *it2 = begin_b_iter(root);
uint32_t saved[40];
for (int i = 0; i < 40; i++)
saved[i] = 0;
// std::string pattern = "f.x";
// Inst *program = compile_regex(pattern);
//
// bool result;
// while ((result = next_match(program, it2, saved))) {
// printf("\nRES: %d\n", result);
// for (int i = 0; i < 40; i++)
// printf("%d, ", saved[i]);
// }
// char c2 = ' ';
// while ((c2 = next_byte(it2)) != '\0')
// printf("%c :wow!:\n", c2);
// free(it2);
// search // uses leaf iterator internally // PCRE2 based
std::vector<std::pair<size_t, size_t>> matches = search_rope(root, "f.x");
for (size_t i = 0; i < matches.size(); i++)
printf("\n%lu %lu", matches[i].first, matches[i].second);
// A rope needs to be freed only once if last action on the rope is
// insert or concat or erase.
// for splits we need to free both left and right separately
free_rope(root);
return 0;
}
*/

View File

@@ -1,70 +0,0 @@
class Buffer
# Simple structs for clarity
Diagnostic = Struct.new(:x0, :y0, :x1, :y1, :message)
Highlight = Struct.new(:x0, :y0, :x1, :y1, :fg, :bg)
VirtualText = Struct.new(:x, :y, :lines)
Cursor = Struct.new(:x, :y)
attr_accessor :text, :cursor, :selection_start,
:diagnostics, :highlights, :virtual_texts
def initialize(initial_text = "")
@text = initial_text
@cursor = Cursor.new(0, 0)
@selection_start = Cursor.new(0, 0)
@diagnostics = []
@highlights = []
@virtual_texts = []
end
# Utility methods
def lines
@text.split("\n")
end
def line_count
lines.size
end
def line(y)
lines[y] || ""
end
def insert(x, y, str)
current_line = lines[y] || ""
before = current_line[0...x] || ""
after = current_line[x..-1] || ""
lines_arr = lines
lines_arr[y] = before + str + after
@text = lines_arr.join("\n")
end
def erase(x0, y0, x1, y1)
lines_arr = lines
if y0 == y1
line = lines_arr[y0] || ""
lines_arr[y0] = line[0...x0] + line[x1..-1].to_s
else
first = lines_arr[y0][0...x0]
last = lines_arr[y1][x1..-1].to_s
lines_arr[y0..y1] = [first + last]
end
@text = lines_arr.join("\n")
end
# Add overlays
def add_diagnostic(x0, y0, x1, y1, message)
@diagnostics << Diagnostic.new(x0, y0, x1, y1, message)
end
def add_highlight(x0, y0, x1, y1, fg: 0xFFFFFF, bg: 0x000000)
@highlights << Highlight.new(x0, y0, x1, y1, fg, bg)
end
def add_virtual_text(x, y, lines)
@virtual_texts << VirtualText.new(x, y, lines)
end
def render()
end
end

View File

@@ -1,57 +0,0 @@
require "ffi"
require_relative "utils"
module C
extend FFI::Library
ffi_lib File.join(__dir__, "../../builds/C-crib.so")
class KeyEvent < FFI::Struct
layout :key_type, :uint8,
:c, :char,
:special_key, :uint8,
:special_modifier, :uint8,
:mouse_x, :uint8,
:mouse_y, :uint8,
:mouse_button, :uint8,
:mouse_state, :uint8,
:mouse_direction, :uint8,
:mouse_modifier, :uint8
def to_s
case KEY_TYPE[self[:key_type]]
when :char
"#<KeyEvent char=#{self[:c].inspect}>"
when :special
"#<KeyEvent special key=#{SPECIAL_KEY[self[:special_key]]} mod=#{MODIFIER[self[:special_modifier]]}>"
when :mouse
"#<KeyEvent mouse x=#{self[:mouse_x]} y=#{self[:mouse_y]} " \
"btn=#{MOUSE_BUTTON[self[:mouse_button]]} state=#{MOUSE_STATE[self[:mouse_state]]} " \
"scroll_dir=#{SCROLL_DIR[self[:mouse_direction]]} mod=#{MODIFIER[self[:mouse_modifier]]}>"
else
"#<KeyEvent type=#{self[:key_type]} unknown=true>"
end
end
end
class Coords < FFI::Struct
layout :x, :int,
:y, :int
def to_ary
[self[:x], self[:y]]
end
def to_s
"#<Coords x=#{self[:x]} y=#{self[:y]}>"
end
end
attach_function :start_screen, [], :void
attach_function :end_screen, [], :void
attach_function :update, [:int, :int, :string, :uint32, :uint32, :uint8], :void
attach_function :render, [], :void
attach_function :set_cursor, [:int, :int, :int], :void
attach_function :read_key, [], KeyEvent.by_value
attach_function :get_size, [], Coords.by_value
attach_function :real_width, [:string], :int
end

243
src/ts.cc Normal file
View File

@@ -0,0 +1,243 @@
#include "../include/ts.h"
#include "../include/editor.h"
#include "../include/rope.h"
#include <cstdint>
#include <fstream>
#include <regex>
#include <string>
#include <unordered_map>
static std::unordered_map<std::string, std::regex> regex_cache;
static const std::regex scm_regex(
R"((@[A-Za-z0-9_.]+)|(;; \#[0-9a-fA-F]{6} \#[0-9a-fA-F]{6} [01] [01] [01] \d+))");
TSQuery *load_query(const char *query_path, Editor *editor) {
const TSLanguage *lang = editor->language;
std::ifstream file(query_path, std::ios::in | std::ios::binary);
if (!file.is_open())
return nullptr;
std::string highlight_query((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
std::smatch match;
std::map<std::string, int> capture_name_cache;
Highlight *c_hl = nullptr;
int i = 0;
int limit = 20;
editor->query_map.resize(limit);
std::string::const_iterator searchStart(highlight_query.cbegin());
while (std::regex_search(searchStart, highlight_query.cend(), match,
scm_regex)) {
std::string mct = match.str();
if (mct.substr(0, 1) == "@") {
std::string capture_name = mct;
if (!capture_name_cache.count(capture_name)) {
if (c_hl) {
if (i >= limit) {
limit += 20;
editor->query_map.resize(limit);
}
editor->query_map[i] = *c_hl;
delete c_hl;
c_hl = nullptr;
}
capture_name_cache[capture_name] = i;
i++;
}
} else if (mct.substr(0, 2) == ";;") {
if (c_hl)
delete c_hl;
c_hl = new Highlight();
c_hl->fg = HEX(mct.substr(4, 6));
c_hl->bg = HEX(mct.substr(12, 6));
int bold = std::stoi(mct.substr(19, 1));
int italic = std::stoi(mct.substr(21, 1));
int underline = std::stoi(mct.substr(23, 1));
c_hl->priority = std::stoi(mct.substr(25));
c_hl->flags = (bold ? CF_BOLD : 0) | (italic ? CF_ITALIC : 0) |
(underline ? CF_UNDERLINE : 0);
}
searchStart = match.suffix().first;
}
if (c_hl)
delete c_hl;
uint32_t error_offset = 0;
TSQueryError error_type = (TSQueryError)0;
TSQuery *q = ts_query_new(lang, highlight_query.c_str(),
(uint32_t)highlight_query.length(), &error_offset,
&error_type);
return q;
}
static inline const TSNode *find_capture_node(const TSQueryMatch &match,
uint32_t capture_id) {
for (uint32_t i = 0; i < match.capture_count; i++)
if (match.captures[i].index == capture_id)
return &match.captures[i].node;
return nullptr;
}
static inline std::string node_text(const TSNode &node, Knot *source) {
uint32_t start = ts_node_start_byte(node);
uint32_t end = ts_node_end_byte(node);
char *text = read(source, start, end - start);
std::string final = std::string(text, end - start);
free(text);
return final;
}
static inline bool ts_predicate(TSQuery *query, const TSQueryMatch &match,
Knot *source) {
uint32_t step_count;
const TSQueryPredicateStep *steps =
ts_query_predicates_for_pattern(query, match.pattern_index, &step_count);
if (!steps || step_count != 4)
return true;
if (source->char_count >= (64 * 1024))
return false;
std::string command;
std::string regex_txt;
uint32_t subject_id = 0;
for (uint32_t i = 0; i < step_count; i++) {
const TSQueryPredicateStep *step = &steps[i];
if (step->type == TSQueryPredicateStepTypeDone)
break;
switch (step->type) {
case TSQueryPredicateStepTypeString: {
uint32_t length = 0;
const char *s =
ts_query_string_value_for_id(query, step->value_id, &length);
if (i == 0)
command.assign(s, length);
else
regex_txt.assign(s, length);
break;
}
case TSQueryPredicateStepTypeCapture: {
subject_id = step->value_id;
break;
}
case TSQueryPredicateStepTypeDone:
break;
}
}
const TSNode *node = find_capture_node(match, subject_id);
std::string subject = node_text(*node, source);
auto it = regex_cache.find(regex_txt);
if (it == regex_cache.end())
it = regex_cache.emplace(regex_txt, std::regex(regex_txt)).first;
const std::regex &re = it->second;
if (command == "match?")
return std::regex_match(subject, re);
else if (command == "not-match?")
return !std::regex_match(subject, re);
return false;
}
const char *read_ts(void *payload, uint32_t byte_index, TSPoint,
uint32_t *bytes_read) {
if (!running) {
*bytes_read = 0;
return "";
}
TSLoad *load = (TSLoad *)payload;
Knot *root = load->editor->root;
if (load->prev)
free(load->prev);
if (byte_index >= root->char_count) {
*bytes_read = 0;
load->prev = nullptr;
return "";
}
uint32_t chunk_size = 4096;
uint32_t remaining = root->char_count - byte_index;
uint32_t len_to_read = remaining > chunk_size ? chunk_size : remaining;
std::shared_lock lock(load->editor->knot_mtx);
char *buffer = read(root, byte_index, len_to_read);
lock.unlock();
load->prev = buffer;
*bytes_read = len_to_read;
return buffer;
}
static inline Highlight *safe_get(std::vector<Highlight> &vec, size_t index) {
if (index >= vec.size())
return nullptr;
return &vec[index];
}
void ts_collect_spans(Editor *editor) {
if (!editor->parser || !editor->root || !editor->query)
return;
TSLoad load = {editor, nullptr};
TSInput tsinput = {
.payload = &load,
.read = read_ts,
.encoding = TSInputEncodingUTF8,
.decode = nullptr,
};
TSTree *tree, *copy = nullptr;
std::unique_lock lock_0(editor->knot_mtx);
if (editor->tree)
copy = ts_tree_copy(editor->tree);
lock_0.unlock();
if (!running)
return;
std::vector<TSInputEdit> edits;
TSInputEdit edit;
if (copy) {
while (editor->edit_queue.pop(edit)) {
edits.push_back(edit);
ts_tree_edit(copy, &edits.back());
}
}
tree = ts_parser_parse(editor->parser, copy, tsinput);
ts_tree_delete(copy);
while (editor->edit_queue.pop(edit)) {
edits.push_back(edit);
ts_tree_edit(tree, &edits.back());
}
lock_0.lock();
if (editor->tree)
ts_tree_delete(editor->tree);
editor->tree = tree;
copy = ts_tree_copy(tree);
lock_0.unlock();
std::shared_lock lock_1(editor->span_mtx);
TSQueryCursor *cursor = ts_query_cursor_new();
ts_query_cursor_exec(cursor, editor->query, ts_tree_root_node(copy));
std::vector<Span> new_spans;
new_spans.reserve(4096);
TSQueryMatch match;
while (ts_query_cursor_next_match(cursor, &match)) {
if (!running)
break;
if (!ts_predicate(editor->query, match, editor->root))
continue;
for (uint32_t i = 0; i < match.capture_count; i++) {
if (!running)
break;
TSQueryCapture cap = match.captures[i];
uint32_t start = ts_node_start_byte(cap.node);
uint32_t end = ts_node_end_byte(cap.node);
Highlight *hl = safe_get(editor->query_map, cap.index);
if (hl)
new_spans.push_back({start, end, hl});
}
}
if (!running) {
ts_tree_delete(copy);
ts_query_cursor_delete(cursor);
if (load.prev)
free(load.prev);
return;
}
lock_1.unlock();
std::sort(new_spans.begin(), new_spans.end());
std::unique_lock lock_2(editor->span_mtx);
editor->spans.swap(new_spans);
lock_2.unlock();
ts_query_cursor_delete(cursor);
if (load.prev)
free(load.prev);
}

87
src/utils.cc Normal file
View File

@@ -0,0 +1,87 @@
extern "C" {
#include "../libs/libgrapheme/grapheme.h"
}
#include "../include/utils.h"
#include <cstdarg>
#include <cstdint>
#include <cstdio>
#include <limits.h>
#include <string.h>
#include <string>
#include <unistd.h>
std::string get_exe_dir() {
char exe_path[PATH_MAX];
ssize_t count = readlink("/proc/self/exe", exe_path, PATH_MAX);
if (count == -1)
return "";
exe_path[count] = '\0';
std::string path(exe_path);
return path.substr(0, path.find_last_of('/'));
}
uint32_t grapheme_strlen(const char *s) {
if (!s)
return 0;
uint32_t count = 0;
const char *p = s;
while (*p) {
uint32_t next = grapheme_next_character_break_utf8(p, UINT32_MAX);
if (!next)
next = 1;
p += next;
count++;
}
return count;
}
uint32_t get_visual_col_from_bytes(const char *line, uint32_t byte_limit) {
if (!line)
return 0;
uint32_t visual_col = 0;
uint32_t current_byte = 0;
uint32_t len = strlen(line);
if (len > 0 && line[len - 1] == '\n')
len--;
while (current_byte < byte_limit && current_byte < len) {
uint32_t inc = grapheme_next_character_break_utf8(line + current_byte,
len - current_byte);
if (current_byte + inc > byte_limit)
break;
current_byte += inc;
visual_col++;
}
return visual_col;
}
uint32_t get_bytes_from_visual_col(const char *line,
uint32_t target_visual_col) {
if (!line)
return 0;
uint32_t visual_col = 0;
uint32_t current_byte = 0;
uint32_t len = strlen(line);
if (len > 0 && line[len - 1] == '\n')
len--;
while (visual_col < target_visual_col && current_byte < len) {
uint32_t inc = grapheme_next_character_break_utf8(line + current_byte,
len - current_byte);
current_byte += inc;
visual_col++;
}
return current_byte;
}
void log(const char *fmt, ...) {
FILE *fp = fopen("/tmp/log.txt", "a");
if (!fp)
return;
va_list args;
va_start(args, fmt);
vfprintf(fp, fmt, args);
va_end(args);
fputc('\n', fp);
fclose(fp);
}