package core; import "core:os" import "core:log" import "core:path/filepath" import "core:mem" import "core:fmt" import "core:math" import "core:slice" import "base:runtime" import "core:strings" import ts "../tree_sitter" import "../theme" ScrollDir :: enum { Up, Down, } ContentType :: enum { Original, Added, } ContentSlice :: struct { type: ContentType, slice: []u8, } Cursor :: struct { col: int, line: int, index: PieceTableIndex, } Selection :: struct { start: Cursor, end: Cursor, } FileBuffer :: struct { allocator: mem.Allocator, directory: string, file_path: string, extension: string, flags: BufferFlagSet, last_col: int, top_line: int, selection: Maybe(Selection), tree: ts.State, history: FileHistory, glyphs: GlyphBuffer, input_buffer: [dynamic]u8, } BufferFlagSet :: bit_set[BufferFlags] BufferFlags :: enum { UnsavedChanges, } FileBufferIter :: struct { cursor: Cursor, buffer: ^FileBuffer, piter: PieceTableIter, hit_end: bool, } // TODO: don't make this panic on nil snapshot buffer_piece_table :: proc(file_buffer: ^FileBuffer) -> ^PieceTable { return &file_buffer.history.piece_table } new_file_buffer_iter_from_beginning :: proc(file_buffer: ^FileBuffer) -> FileBufferIter { return FileBufferIter { buffer = file_buffer, piter = new_piece_table_iter(buffer_piece_table(file_buffer)) }; } new_file_buffer_iter_with_cursor :: proc(file_buffer: ^FileBuffer, cursor: Cursor) -> FileBufferIter { return FileBufferIter { buffer = file_buffer, cursor = cursor, piter = new_piece_table_iter_from_index(buffer_piece_table(file_buffer), cursor.index) }; } new_file_buffer_iter :: proc{new_file_buffer_iter_from_beginning, new_file_buffer_iter_with_cursor}; file_buffer_end :: proc(buffer: ^FileBuffer) -> Cursor { return Cursor { col = 0, line = 0, index = new_piece_table_index_from_end(buffer_piece_table(buffer)) }; } iterate_file_buffer :: proc(it: ^FileBufferIter) -> (character: u8, idx: PieceTableIndex, cond: bool) { character, idx, cond = iterate_piece_table_iter(&it.piter) it.cursor.index = it.piter.index it.hit_end = it.piter.hit_end if cond && !it.hit_end { if character == '\n' { it.cursor.col = 0 it.cursor.line += 1 } else { it.cursor.col += 1 } } return } // TODO: figure out how to give the first character of the buffer iterate_file_buffer_reverse :: proc(it: ^FileBufferIter) -> (character: u8, idx: PieceTableIndex, cond: bool) { character, idx, cond = iterate_piece_table_iter_reverse(&it.piter) it.cursor.index = it.piter.index it.hit_end = it.piter.hit_end if cond && !it.hit_end { if it.cursor.col > 0 { it.cursor.col -= 1 } else if it.cursor.line > 0 { line_length := file_buffer_line_length(it.buffer, it.cursor.index) if line_length < 0 { line_length = 0 } it.cursor.line -= 1 it.cursor.col = line_length } } return } get_character_at_iter :: proc(it: FileBufferIter) -> u8 { return get_character_at_piece_table_index(buffer_piece_table(it.buffer), it.cursor.index); } IterProc :: proc(it: ^FileBufferIter) -> (character: u8, idx: PieceTableIndex, cond: bool); UntilProc :: proc(it: ^FileBufferIter, iter_proc: IterProc) -> bool; iterate_file_buffer_until :: proc(it: ^FileBufferIter, until_proc: UntilProc) { for until_proc(it, iterate_file_buffer) {} } iterate_file_buffer_until_reverse :: proc(it: ^FileBufferIter, until_proc: UntilProc) { for until_proc(it, iterate_file_buffer_reverse) {} } iterate_peek :: proc(it: ^FileBufferIter, iter_proc: IterProc) -> (character: u8, peek_it: FileBufferIter, cond: bool) { peek_it = it^; character, _, cond = iter_proc(&peek_it); if !cond { return character, peek_it, cond; } character = get_character_at_iter(peek_it); return character, peek_it, cond; } until_non_whitespace :: proc(it: ^FileBufferIter, iter_proc: IterProc) -> bool { before_it := it^; if character, _, cond := iter_proc(it); cond && strings.is_space(rune(character)) { return cond; } it^ = before_it; return false; } until_before_non_whitespace :: proc(it: ^FileBufferIter, iter_proc: IterProc) -> bool { if character, peek_it, cond := iterate_peek(it, iter_proc); cond && strings.is_space(rune(character)) { it^ = peek_it; return true; } return false; } until_non_alpha_num :: proc(it: ^FileBufferIter, iter_proc: IterProc) -> bool { // TODO: make this global set, _ := strings.ascii_set_make("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"); before_it := it^; if character, _, cond := iter_proc(it); cond && strings.ascii_set_contains(set, character) { return cond; } it^ = before_it; return false; } until_before_non_alpha_num :: proc(it: ^FileBufferIter, iter_proc: IterProc) -> bool { // TODO: make this global set, _ := strings.ascii_set_make("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"); if character, peek_it, cond := iterate_peek(it, iter_proc); cond && strings.ascii_set_contains(set, character) { it^ = peek_it; return cond; } return false; } until_alpha_num :: proc(it: ^FileBufferIter, iter_proc: IterProc) -> bool { set, _ := strings.ascii_set_make("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"); before_it := it^; if character, _, cond := iter_proc(it); cond && !strings.ascii_set_contains(set, character) { return cond; } it^ = before_it; return false; } until_before_alpha_num :: proc(it: ^FileBufferIter, iter_proc: IterProc) -> bool { set, _ := strings.ascii_set_make("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"); if character, peek_it, cond := iterate_peek(it, iter_proc); cond && !strings.ascii_set_contains(set, character) { it^ = peek_it; return cond; } return false; } until_before_alpha_num_or_whitespace :: proc(it: ^FileBufferIter, iter_proc: IterProc) -> bool { set, _ := strings.ascii_set_make("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"); if character, peek_it, cond := iterate_peek(it, iter_proc); cond && (!strings.ascii_set_contains(set, character) && !strings.is_space(rune(character))) { it^ = peek_it; return cond; } return false; } until_start_of_word :: proc(it: ^FileBufferIter, iter_proc: IterProc) -> bool { set, _ := strings.ascii_set_make("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"); // if on a symbol go to next symbol or word current_character := get_character_at_iter(it^); if !strings.ascii_set_contains(set, current_character) && !strings.is_space(rune(current_character)) { _, _, cond := iter_proc(it); if !cond { return false; } for until_alpha_num(it, iter_proc) {} return false; } for until_non_alpha_num(it, iter_proc) {} for until_non_whitespace(it, iter_proc) {} return false; } until_end_of_word :: proc(it: ^FileBufferIter, iter_proc: IterProc) -> bool { set, _ := strings.ascii_set_make("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"); current_character := get_character_at_iter(it^); if character, peek_it, cond := iterate_peek(it, iter_proc); strings.ascii_set_contains(set, current_character) { // if current charater is a word if strings.is_space(rune(character)) { it^ = peek_it; for until_non_whitespace(it, iter_proc) {} } if strings.ascii_set_contains(set, character) { // we are within a word for until_before_non_alpha_num(it, iter_proc) {} } else { // we are at the start of a word for until_before_alpha_num_or_whitespace(it, iter_proc) {} } } else if character, peek_it, cond := iterate_peek(it, iter_proc); !strings.ascii_set_contains(set, current_character) { // if current charater is a symbol if strings.is_space(rune(character)) { it^ = peek_it; for until_non_whitespace(it, iter_proc) {} character = get_character_at_iter(it^); } if !strings.ascii_set_contains(set, character) { // we are within a run of symbols for until_before_alpha_num_or_whitespace(it, iter_proc) {} } else { // we are at the start of a run of symbols for until_before_non_alpha_num(it, iter_proc) {} } } return false; } until_double_quote :: proc(it: ^FileBufferIter, iter_proc: IterProc) -> bool { before_it := it^; character, _, cond := iter_proc(it); if !cond { return cond; } // skip over escaped characters if character == '\\' { _, _, cond = iter_proc(it); } else if character == '"' { it^ = before_it; return false; } return cond; } until_single_quote :: proc(it: ^FileBufferIter, iter_proc: IterProc) -> bool { before_it := it^; character, _, cond := iter_proc(it); if !cond { return cond; } // skip over escaped characters if character == '\\' { _, _, cond = iter_proc(it); } else if character == '\'' { it^ = before_it; return false; } return cond; } until_line_break :: proc(it: ^FileBufferIter, iter_proc: IterProc) -> (cond: bool) { if get_character_at_piece_table_index(buffer_piece_table(it.buffer), it.cursor.index) == '\n' { return false; } _, _, cond = iter_proc(it); return cond; } update_file_buffer_index_from_cursor :: proc(buffer: ^FileBuffer) { it := new_file_buffer_iter(buffer); before_it := new_file_buffer_iter(buffer); line_length := 0; rendered_line := 0; for character in iterate_file_buffer(&it) { if line_length == buffer.history.cursor.col && rendered_line == buffer.history.cursor.line { break; } if character == '\n' { rendered_line += 1; line_length = 0; } else { line_length += 1; } before_it = it; } // FIXME: just swap cursors buffer.history.cursor.index = before_it.cursor.index; update_file_buffer_scroll(buffer); } file_buffer_line_length :: proc(buffer: ^FileBuffer, index: PieceTableIndex) -> int { line_length := 0; // if len(buffer.content_slices) <= 0 do return line_length first_character := get_character_at_piece_table_index(buffer_piece_table(buffer), index); left_it := new_piece_table_iter_from_index(buffer_piece_table(buffer), index); if first_character == '\n' { iterate_piece_table_iter_reverse(&left_it); } for character in iterate_piece_table_iter_reverse(&left_it) { if character == '\n' { break; } line_length += 1; } right_it := new_piece_table_iter_from_index(buffer_piece_table(buffer), index); first := true; for character in iterate_piece_table_iter(&right_it) { if character == '\n' { break; } if !first { line_length += 1; } first = false; } return line_length; } move_cursor_start_of_line :: proc(buffer: ^FileBuffer, cursor: Maybe(^Cursor) = nil) { cursor := cursor; if cursor == nil { cursor = &buffer.history.cursor; } if cursor.?.col > 0 { it := new_file_buffer_iter_with_cursor(buffer, cursor.?^); for _ in iterate_file_buffer_reverse(&it) { if it.cursor.col <= 0 { break; } } cursor.?^ = it.cursor; } buffer.last_col = cursor.?.col } move_cursor_end_of_line :: proc(buffer: ^FileBuffer, stop_at_end: bool = true, cursor: Maybe(^Cursor) = nil) { cursor := cursor; if cursor == nil { cursor = &buffer.history.cursor; } buffer.last_col = cursor.?.col it := new_file_buffer_iter_with_cursor(buffer, cursor.?^); line_length := file_buffer_line_length(buffer, it.cursor.index); if stop_at_end { line_length -= 1; } if cursor.?.col < line_length { for _ in iterate_file_buffer(&it) { if it.cursor.col >= line_length { break; } } cursor.?^ = it.cursor; } buffer.last_col = cursor.?.col } move_cursor_up :: proc(buffer: ^FileBuffer, amount: int = 1, cursor: Maybe(^Cursor) = nil) { cursor := cursor; if cursor == nil { cursor = &buffer.history.cursor; } if cursor.?.line > 0 { current_line := cursor.?.line; current_col := cursor.?.col; it := new_file_buffer_iter_with_cursor(buffer, cursor.?^); for _ in iterate_file_buffer_reverse(&it) { if it.cursor.line <= current_line-amount || it.cursor.line < 1 { break; } } // the `it.cursor.col > 0` is here because after the above loop, the // iterator is left on the new line instead of the last character of the line if it.cursor.col > current_col || it.cursor.col > 0 { for _ in iterate_file_buffer_reverse(&it) { if it.cursor.col <= current_col { break; } } } cursor.?^ = it.cursor; } if cursor.?.col < buffer.last_col && file_buffer_line_length(buffer, cursor.?.index)-1 >= cursor.?.col { last_col := buffer.last_col move_cursor_right(buffer, amt = buffer.last_col - cursor.?.col, cursor = cursor) buffer.last_col = last_col } update_file_buffer_scroll(buffer, cursor); } move_cursor_down :: proc(buffer: ^FileBuffer, amount: int = 1, cursor: Maybe(^Cursor) = nil) { cursor := cursor; if cursor == nil { cursor = &buffer.history.cursor; } current_line := cursor.?.line; current_col := cursor.?.col; it := new_file_buffer_iter_with_cursor(buffer, cursor.?^); for _ in iterate_file_buffer(&it) { if it.cursor.line >= current_line+amount { break; } } if it.hit_end { return } line_length := file_buffer_line_length(buffer, it.cursor.index); if it.cursor.col < line_length-1 && it.cursor.col < current_col { for _ in iterate_file_buffer(&it) { if it.cursor.col >= line_length-1 || it.cursor.col >= current_col { break; } } } cursor.?^ = it.cursor; if cursor.?.col < buffer.last_col && file_buffer_line_length(buffer, cursor.?.index)-1 >= cursor.?.col { last_col := buffer.last_col move_cursor_right(buffer, amt = buffer.last_col - cursor.?.col, cursor = cursor) buffer.last_col = last_col } update_file_buffer_scroll(buffer, cursor); } move_cursor_left :: proc(buffer: ^FileBuffer, cursor: Maybe(^Cursor) = nil) { cursor := cursor; if cursor == nil { cursor = &buffer.history.cursor; } buffer.last_col = cursor.?.col if cursor.?.col > 0 { it := new_file_buffer_iter_with_cursor(buffer, cursor.?^); iterate_file_buffer_reverse(&it); cursor.?^ = it.cursor; } } move_cursor_right :: proc(buffer: ^FileBuffer, stop_at_end: bool = true, amt: int = 1, cursor: Maybe(^Cursor) = nil) { cursor := cursor; if cursor == nil { cursor = &buffer.history.cursor; } buffer.last_col = cursor.?.col it := new_file_buffer_iter_with_cursor(buffer, cursor.?^); line_length := file_buffer_line_length(buffer, it.cursor.index); for _ in 0.. Selection { return { start = cursor, end = cursor, }; } new_selection_span :: proc(start: Cursor, end: Cursor) -> Selection { return { start = start, end = end, }; } new_selection_current_line :: proc(buffer: ^FileBuffer, cursor: Cursor) -> Selection { start := cursor end := cursor move_cursor_start_of_line(buffer, &start) move_cursor_end_of_line(buffer, true, &end) return { start = start, end = end, } } new_selection :: proc{new_selection_zero_length, new_selection_span, new_selection_current_line}; swap_selections :: proc(selection: Selection) -> (swapped: Selection) { swapped = selection if is_selection_inverted(selection) { swapped.start = selection.end swapped.end = selection.start } return swapped } // TODO: don't access PieceTableIndex directly is_selection_inverted :: proc(selection: Selection) -> bool { return selection.start.index.chunk_index > selection.end.index.chunk_index || (selection.start.index.chunk_index == selection.end.index.chunk_index && selection.start.index.char_index > selection.end.index.char_index) } selection_length :: proc(buffer: ^FileBuffer, selection: Selection) -> int { selection := selection it := new_file_buffer_iter_with_cursor(buffer, selection.start) length := 0 for !it.hit_end && !is_selection_inverted(selection) { iterate_file_buffer(&it); selection.start = it.cursor length += 1 } return length } new_virtual_file_buffer :: proc(allocator := context.allocator) -> FileBuffer { context.allocator = allocator; width := 256; height := 256; buffer := FileBuffer { allocator = allocator, file_path = "virtual_buffer", history = make_history(), glyphs = make_glyph_buffer(width, height), input_buffer = make([dynamic]u8, 0, 1024), }; return buffer; } new_file_buffer :: proc(allocator: mem.Allocator, file_path: string, base_dir: string = "") -> (FileBuffer, Error) { context.allocator = allocator; fmt.eprintln("attempting to open", file_path); fd, err := os.open(file_path); if err != nil { return FileBuffer{}, make_error(ErrorType.FileIOError, fmt.aprintf("failed to open file: errno=%x", err)); } defer os.close(fd); fi, fstat_err := os.fstat(fd); if fstat_err != nil { return FileBuffer{}, make_error(ErrorType.FileIOError, fmt.aprintf("failed to get file info: errno=%x", fstat_err)); } dir: string; if base_dir != "" { dir = base_dir; } else { dir = filepath.dir(fi.fullpath); } extension := filepath.ext(fi.fullpath); file_type: ts.LanguageType = .None if extension == ".odin" { file_type = .Odin } else if extension == ".rs" { file_type = .Rust } else if extension == ".json" { file_type = .Json } if original_content, success := os.read_entire_file_from_handle(fd); success { defer delete(original_content) content := make([]u8, len(original_content)) copy_slice(content, original_content) width := 256; height := 256; fmt.eprintln("file path", fi.fullpath[:]); buffer := FileBuffer { allocator = allocator, directory = dir, file_path = fi.fullpath, // TODO: fix this windows issue // file_path = fi.fullpath[4:], extension = extension, // TODO: derive language type from extension tree = ts.make_state(file_type), history = make_history(content), glyphs = make_glyph_buffer(width, height), input_buffer = make([dynamic]u8, 0, 1024), }; ts.parse_buffer(&buffer.tree, tree_sitter_file_buffer_input(&buffer)) return buffer, error(); } else { return FileBuffer{}, error(ErrorType.FileIOError, fmt.aprintf("failed to read from file")); } } tree_sitter_file_buffer_input :: proc(buffer: ^FileBuffer) -> ts.Input { read :: proc "c" (payload: rawptr, byte_index: u32, position: ts.Point, bytes_read: ^u32) -> ^u8 { context = runtime.default_context() buffer := transmute(^FileBuffer)payload if iter, ok := new_piece_table_iter_from_byte_offset(&buffer.history.piece_table, int(byte_index)); ok { bytes := iter.t.chunks[iter.index.chunk_index][iter.index.char_index:] bytes_read^ = u32(len(bytes)) return raw_data(bytes) } else { bytes_read^ = 0 return nil } } return ts.Input { payload = buffer, read = read, encoding = .UTF8, } } save_buffer_to_disk :: proc(state: ^State, buffer: ^FileBuffer) -> (error: os.Error) { fd := os.open(buffer.file_path, flags = os.O_WRONLY | os.O_TRUNC | os.O_CREATE) or_return; defer os.close(fd); offset: i64 = 0 for chunk in buffer_piece_table(buffer).chunks { os.write(fd, chunk) or_return offset += i64(len(chunk)) } os.flush(fd) log.infof("written %v bytes", offset) buffer.flags -= { .UnsavedChanges } return } // TODO: replace this with arena for the file buffer free_file_buffer :: proc(buffer: ^FileBuffer) { ts.delete_state(&buffer.tree) free_history(&buffer.history) delete(buffer.glyphs.buffer) delete(buffer.input_buffer) } color_character :: proc(buffer: ^FileBuffer, start: Cursor, end: Cursor, palette_index: theme.PaletteColor) { start, end := start, end; if end.line < buffer.top_line { return; } if start.line < buffer.top_line { start.line = 0; } else { start.line -= buffer.top_line; } if end.line >= buffer.top_line + buffer.glyphs.height { end.line = buffer.glyphs.height - 1; end.col = buffer.glyphs.width - 1; } else { end.line -= buffer.top_line; } for j in start.line..=end.line { start_col := start.col; end_col := end.col; if j > start.line && j < end.line { start_col = 0; end_col = buffer.glyphs.width; } else if j < end.line { end_col = buffer.glyphs.width; } else if j > start.line && j == end.line { start_col = 0; } for i in start_col.. 0 { cursor_x = x + padding + line_length * state.source_font_width; cursor_y = cursor_y + num_line_break * state.source_font_height; } else { cursor_x += line_length * state.source_font_width; } draw_rect(state, cursor_x, cursor_y, state.source_font_width, state.source_font_height, .Blue); } } // TODO: replace with glyph_buffer.draw_glyph_buffer for j in 0..= selection.start.line && begin+j <= selection.end.line { if begin+j == selection.start.line && selection.start.line == selection.end.line { width = (selection.end.col - selection.start.col) * state.source_font_width; sel_x += selection.start.col * state.source_font_width; } else if begin+j == selection.end.line { width = selection.end.col * state.source_font_width; } else { if begin+j == selection.start.line { width = (line_length - selection.start.col) * state.source_font_width; sel_x += selection.start.col * state.source_font_width; } else { width = line_length * state.source_font_width; } } } draw_rect(state, sel_x, text_y, width, state.source_font_height, .Green); } } } update_file_buffer_scroll :: proc(buffer: ^FileBuffer, cursor: Maybe(^Cursor) = nil) { cursor := cursor; if cursor == nil { cursor = &buffer.history.cursor; } if buffer.glyphs.height < 5 { buffer.top_line = cursor.?.line } else if cursor.?.line > (buffer.top_line + buffer.glyphs.height - 5) { buffer.top_line = math.max(cursor.?.line - buffer.glyphs.height + 5, 0); } else if cursor.?.line < (buffer.top_line + 5) { buffer.top_line = math.max(cursor.?.line - 5, 0); } } // TODO: don't mangle cursor scroll_file_buffer :: proc(buffer: ^FileBuffer, dir: ScrollDir, cursor: Maybe(^Cursor) = nil) { switch dir { case .Up: { move_cursor_up(buffer, 20, cursor); } case .Down: { move_cursor_down(buffer, 20, cursor); } } } insert_content :: proc(buffer: ^FileBuffer, to_be_inserted: []u8, append_to_end: bool = false) { if len(to_be_inserted) == 0 { return; } buffer.flags += { .UnsavedChanges } index := buffer.history.cursor.index if !append_to_end else new_piece_table_index_from_end(buffer_piece_table(buffer)) insert_text(buffer_piece_table(buffer), to_be_inserted, buffer.history.cursor.index) if !append_to_end { update_file_buffer_index_from_cursor(buffer); move_cursor_right(buffer, false, amt = len(to_be_inserted) - 1); } ts.parse_buffer(&buffer.tree, tree_sitter_file_buffer_input(buffer)) } delete_content_from_buffer_cursor :: proc(buffer: ^FileBuffer, amount: int) { if amount <= len(buffer.input_buffer) { runtime.resize(&buffer.input_buffer, len(buffer.input_buffer)-amount); } else { buffer.flags += { .UnsavedChanges } amount := amount - len(buffer.input_buffer); runtime.clear(&buffer.input_buffer); // Calculate proper line/col values it := new_file_buffer_iter_with_cursor(buffer, buffer.history.cursor); iterate_file_buffer_reverse(&it) delete_text(buffer_piece_table(buffer), &buffer.history.cursor.index) buffer.history.cursor.line = it.cursor.line buffer.history.cursor.col = it.cursor.col } ts.parse_buffer(&buffer.tree, tree_sitter_file_buffer_input(buffer)) } delete_content_from_selection :: proc(buffer: ^FileBuffer, selection: ^Selection) { buffer.flags += { .UnsavedChanges } selection^ = swap_selections(selection^) delete_text_in_span(buffer_piece_table(buffer), &selection.start.index, &selection.end.index) buffer.history.cursor.index = selection.start.index ts.parse_buffer(&buffer.tree, tree_sitter_file_buffer_input(buffer)) } delete_content :: proc{delete_content_from_buffer_cursor, delete_content_from_selection};