diff --git a/.gitignore b/.gitignore index 0d217ec..a4fee73 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,10 @@ .vscode -tests/tmp_* +tests/* +!tests/.keep builds/* !builds/.keep + +src/cpp/__tmp__ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2406848 --- /dev/null +++ b/Makefile @@ -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) diff --git a/builds/.keep b/__old__/builds/.keep similarity index 100% rename from builds/.keep rename to __old__/builds/.keep diff --git a/__old__/crib.rb b/__old__/crib.rb new file mode 100755 index 0000000..650d9fd --- /dev/null +++ b/__old__/crib.rb @@ -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 diff --git a/__old__/editor.rb b/__old__/editor.rb new file mode 100644 index 0000000..70a9087 --- /dev/null +++ b/__old__/editor.rb @@ -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 diff --git a/__old__/fm.rb b/__old__/fm.rb new file mode 100644 index 0000000..897b478 --- /dev/null +++ b/__old__/fm.rb @@ -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 diff --git a/__old__/ide.rb b/__old__/ide.rb new file mode 100644 index 0000000..e61e9d2 --- /dev/null +++ b/__old__/ide.rb @@ -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 diff --git a/__old__/mod.rb b/__old__/mod.rb new file mode 100644 index 0000000..ed56916 --- /dev/null +++ b/__old__/mod.rb @@ -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 + "#" + when :special + "#" + when :mouse + "#" + else + "#" + end + end + end + + class Coords < FFI::Struct + layout :x, :int, + :y, :int + + def to_ary + [self[:x], self[:y]] + end + + def to_s + "#" + end + end + + class Size < FFI::Struct + layout :rows, :int, + :cols, :int + + def to_ary + [self[:rows], self[:cols]] + end + + def to_s + "#" + 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 diff --git a/__old__/ts_rb.rb b/__old__/ts_rb.rb new file mode 100644 index 0000000..f9556c2 --- /dev/null +++ b/__old__/ts_rb.rb @@ -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), +} diff --git a/src/ruby/utils.rb b/__old__/utils.rb similarity index 61% rename from src/ruby/utils.rb rename to __old__/utils.rb index 3d0fcd3..09ac361 100644 --- a/src/ruby/utils.rb +++ b/__old__/utils.rb @@ -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 diff --git a/bin/crib b/bin/crib new file mode 100755 index 0000000..a56937b Binary files /dev/null and b/bin/crib differ diff --git a/bin/crib-dbg b/bin/crib-dbg new file mode 100755 index 0000000..e9757f3 Binary files /dev/null and b/bin/crib-dbg differ diff --git a/build/debug/editor.d b/build/debug/editor.d new file mode 100644 index 0000000..d8e4993 --- /dev/null +++ b/build/debug/editor.d @@ -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: diff --git a/build/debug/editor.dwo b/build/debug/editor.dwo new file mode 100644 index 0000000..b145722 Binary files /dev/null and b/build/debug/editor.dwo differ diff --git a/build/debug/input.d b/build/debug/input.d new file mode 100644 index 0000000..b303603 --- /dev/null +++ b/build/debug/input.d @@ -0,0 +1,2 @@ +build/debug/input.o: src/input.cc src/../include/ui.h +src/../include/ui.h: diff --git a/build/debug/input.dwo b/build/debug/input.dwo new file mode 100644 index 0000000..6f347ed Binary files /dev/null and b/build/debug/input.dwo differ diff --git a/build/debug/main.d b/build/debug/main.d new file mode 100644 index 0000000..54d87f6 --- /dev/null +++ b/build/debug/main.d @@ -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: diff --git a/build/debug/main.dwo b/build/debug/main.dwo new file mode 100644 index 0000000..9e84348 Binary files /dev/null and b/build/debug/main.dwo differ diff --git a/build/debug/renderer.d b/build/debug/renderer.d new file mode 100644 index 0000000..2f43f44 --- /dev/null +++ b/build/debug/renderer.d @@ -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: diff --git a/build/debug/renderer.dwo b/build/debug/renderer.dwo new file mode 100644 index 0000000..d1f2c82 Binary files /dev/null and b/build/debug/renderer.dwo differ diff --git a/build/debug/rope.d b/build/debug/rope.d new file mode 100644 index 0000000..d4d0598 --- /dev/null +++ b/build/debug/rope.d @@ -0,0 +1,2 @@ +build/debug/rope.o: src/rope.cc src/../include/rope.h +src/../include/rope.h: diff --git a/build/debug/rope.dwo b/build/debug/rope.dwo new file mode 100644 index 0000000..33bd675 Binary files /dev/null and b/build/debug/rope.dwo differ diff --git a/build/debug/ts.d b/build/debug/ts.d new file mode 100644 index 0000000..00c3c7a --- /dev/null +++ b/build/debug/ts.d @@ -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: diff --git a/build/debug/ts.dwo b/build/debug/ts.dwo new file mode 100644 index 0000000..c0b5d24 Binary files /dev/null and b/build/debug/ts.dwo differ diff --git a/build/debug/unicode_width/unicode_width.d b/build/debug/unicode_width/unicode_width.d new file mode 100644 index 0000000..f460fc9 --- /dev/null +++ b/build/debug/unicode_width/unicode_width.d @@ -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: diff --git a/build/debug/unicode_width/unicode_width.dwo b/build/debug/unicode_width/unicode_width.dwo new file mode 100644 index 0000000..88f29da Binary files /dev/null and b/build/debug/unicode_width/unicode_width.dwo differ diff --git a/build/debug/utils.d b/build/debug/utils.d new file mode 100644 index 0000000..222be35 --- /dev/null +++ b/build/debug/utils.d @@ -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: diff --git a/build/debug/utils.dwo b/build/debug/utils.dwo new file mode 100644 index 0000000..16fcbf1 Binary files /dev/null and b/build/debug/utils.dwo differ diff --git a/build/release/editor.d b/build/release/editor.d new file mode 100644 index 0000000..7cb6e09 --- /dev/null +++ b/build/release/editor.d @@ -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: diff --git a/build/release/input.d b/build/release/input.d new file mode 100644 index 0000000..65c4661 --- /dev/null +++ b/build/release/input.d @@ -0,0 +1,2 @@ +build/release/input.o: src/input.cc src/../include/ui.h +src/../include/ui.h: diff --git a/build/release/main.d b/build/release/main.d new file mode 100644 index 0000000..a8f40c6 --- /dev/null +++ b/build/release/main.d @@ -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: diff --git a/build/release/renderer.d b/build/release/renderer.d new file mode 100644 index 0000000..607ea14 --- /dev/null +++ b/build/release/renderer.d @@ -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: diff --git a/build/release/rope.d b/build/release/rope.d new file mode 100644 index 0000000..c7fba23 --- /dev/null +++ b/build/release/rope.d @@ -0,0 +1,2 @@ +build/release/rope.o: src/rope.cc src/../include/rope.h +src/../include/rope.h: diff --git a/build/release/ts.d b/build/release/ts.d new file mode 100644 index 0000000..2654aa3 --- /dev/null +++ b/build/release/ts.d @@ -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: diff --git a/build/release/unicode_width/unicode_width.d b/build/release/unicode_width/unicode_width.d new file mode 100644 index 0000000..819ffce --- /dev/null +++ b/build/release/unicode_width/unicode_width.d @@ -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: diff --git a/build/release/utils.d b/build/release/utils.d new file mode 100644 index 0000000..349d977 --- /dev/null +++ b/build/release/utils.d @@ -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: diff --git a/compile.sh b/compile.sh deleted file mode 100755 index 53be66a..0000000 --- a/compile.sh +++ /dev/null @@ -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" diff --git a/crib.rb b/crib.rb deleted file mode 100755 index 1a402d7..0000000 --- a/crib.rb +++ /dev/null @@ -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) diff --git a/grammar/ruby.scm b/grammar/ruby.scm new file mode 100644 index 0000000..62454ca --- /dev/null +++ b/grammar/ruby.scm @@ -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) diff --git a/include/.keep b/include/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/include/editor.h b/include/editor.h new file mode 100644 index 0000000..ad80229 --- /dev/null +++ b/include/editor.h @@ -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 +#include +#include + +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 &spans; + size_t index = 0; + std::vector active; + + SpanCursor(const std::vector &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(&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(&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 edit_queue; // Tree-sitter edit queue + std::vector query_map; // Tree-sitter query map + int *folded; // folded lines indexed by line number - cached form of + // folded_node + std::vector spans; + std::map 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); diff --git a/include/rope.h b/include/rope.h new file mode 100644 index 0000000..5f29da4 --- /dev/null +++ b/include/rope.h @@ -0,0 +1,157 @@ +#ifndef ROPE_HPP +#define ROPE_HPP + +#include +#include + +#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> 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 diff --git a/include/ts.h b/include/ts.h new file mode 100644 index 0000000..f175ae7 --- /dev/null +++ b/include/ts.h @@ -0,0 +1,7 @@ +#pragma once +#include "./editor.h" + +#define HEX(s) (static_cast(std::stoul(s, nullptr, 16))) + +TSQuery *load_query(const char *query_path, Editor *editor); +void ts_collect_spans(Editor *editor); diff --git a/src/headers/header.hpp b/include/ui.h similarity index 72% rename from src/headers/header.hpp rename to include/ui.h index e456d50..adc2f1b 100644 --- a/src/headers/header.hpp +++ b/include/ui.h @@ -1,6 +1,8 @@ #pragma once +#include #include #include +#include #include #include #include @@ -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 screen; // size rows*cols extern std::vector old_screen; extern std::mutex screen_mutex; +extern std::atomic 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); diff --git a/include/utils.h b/include/utils.h new file mode 100644 index 0000000..0ee2219 --- /dev/null +++ b/include/utils.h @@ -0,0 +1,32 @@ +#include +#include +#include + +template struct Queue { + std::queue q; + std::mutex m; + + void push(T val) { + std::lock_guard lock(m); + q.push(val); + } + bool pop(T &val) { + std::lock_guard lock(m); + if (q.empty()) + return false; + val = q.front(); + q.pop(); + return true; + } + bool empty() { + std::lock_guard 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(); diff --git a/src/.keep b/src/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/src/editor.cc b/src/editor.cc new file mode 100644 index 0000000..138f567 --- /dev/null +++ b/src/editor.cc @@ -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 +#include + +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(len)); + if (!buf) + return nullptr; + if (file.read(buf, len)) { + *out_len = static_cast(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> 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); +} diff --git a/src/cpp/input.cpp b/src/input.cc similarity index 95% rename from src/cpp/input.cpp rename to src/input.cc index 21a3006..483a395 100644 --- a/src/cpp/input.cpp +++ b/src/input.cc @@ -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); } } diff --git a/src/main.cc b/src/main.cc new file mode 100644 index 0000000..a1c42cd --- /dev/null +++ b/src/main.cc @@ -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 +#include +#include +#include +#include +#include + +std::atomic running{true}; +Queue event_queue; + +std::atomic render_frames{0}; +std::atomic 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(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; +} diff --git a/src/cpp/renderer.cpp b/src/renderer.cc similarity index 60% rename from src/cpp/renderer.cpp rename to src/renderer.cc index 8692e0d..9d36fa1 100644 --- a/src/cpp/renderer.cpp +++ b/src/renderer.cc @@ -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 screen; std::vector 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 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 lock(screen_mutex); - std::string out; - // reserve a conservative amount to avoid repeated reallocs out.reserve(static_cast(rows) * static_cast(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 - out.append(new_cell.utf8); + } 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); } diff --git a/src/rope.cc b/src/rope.cc new file mode 100644 index 0000000..9fbed6c --- /dev/null +++ b/src/rope.cc @@ -0,0 +1,858 @@ +#include "../include/rope.h" +#include +#include +#include +#include +#include +#include +#include +#include + +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> search_rope(Knot *root, + const char *pattern) { + std::vector> 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> 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; +} +*/ diff --git a/src/ruby/editor.rb b/src/ruby/editor.rb deleted file mode 100644 index 293c80e..0000000 --- a/src/ruby/editor.rb +++ /dev/null @@ -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 diff --git a/src/ruby/mod.rb b/src/ruby/mod.rb deleted file mode 100644 index 0383cd3..0000000 --- a/src/ruby/mod.rb +++ /dev/null @@ -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 - "#" - when :special - "#" - when :mouse - "#" - else - "#" - end - end - end - - class Coords < FFI::Struct - layout :x, :int, - :y, :int - - def to_ary - [self[:x], self[:y]] - end - - def to_s - "#" - 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 diff --git a/src/ts.cc b/src/ts.cc new file mode 100644 index 0000000..9ef6f38 --- /dev/null +++ b/src/ts.cc @@ -0,0 +1,243 @@ +#include "../include/ts.h" +#include "../include/editor.h" +#include "../include/rope.h" +#include +#include +#include +#include +#include + +static std::unordered_map 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(file)), + std::istreambuf_iterator()); + std::smatch match; + std::map 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 &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 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 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); +} diff --git a/src/utils.cc b/src/utils.cc new file mode 100644 index 0000000..fefab2a --- /dev/null +++ b/src/utils.cc @@ -0,0 +1,87 @@ +extern "C" { +#include "../libs/libgrapheme/grapheme.h" +} +#include "../include/utils.h" +#include +#include +#include +#include +#include +#include +#include + +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); +}