package main Piece_Source :: enum { Original, Add, } Piece :: struct { source: Piece_Source, start: int, len: int, } Line_Index :: struct { starts: [dynamic]int, dirty: bool, } Buffer_Edit_Kind :: enum { Insert, Delete, Replace, } Buffer_Edit :: struct { kind: Buffer_Edit_Kind, offset: int, text: [dynamic]u8, replacement: [dynamic]u8, } Buffer :: struct { original: string, add: [dynamic]u8, pieces: [dynamic]Piece, undo: [dynamic]Buffer_Edit, redo: [dynamic]Buffer_Edit, line_index: Line_Index, version: int, } Cursor :: struct { offset: int, wanted_column: int, } buffer_make :: proc(original: string) -> Buffer { buffer: Buffer buffer.original = original buffer.line_index.dirty = true if len(original) > 0 { append(&buffer.pieces, Piece{.Original, 0, len(original)}) } return buffer } buffer_len :: proc(buffer: ^Buffer) -> int { total := 0 for piece in buffer.pieces { total += piece.len } return total } buffer_insert :: proc(buffer: ^Buffer, offset: int, text: string) { if len(text) == 0 do return edit_text := clone_bytes(transmute([]u8)text) buffer_push_edit(&buffer.undo, Buffer_Edit{kind = .Insert, offset = clamp_int(offset, 0, buffer_len(buffer)), text = edit_text}) buffer_clear_edits(&buffer.redo) buffer_insert_raw(buffer, offset, text) } buffer_insert_raw :: proc(buffer: ^Buffer, offset: int, text: string) { buffer_insert_raw_internal(buffer, offset, text, true) } buffer_insert_raw_internal :: proc(buffer: ^Buffer, offset: int, text: string, bump_version: bool) { if len(text) == 0 do return insert_at := clamp_int(offset, 0, buffer_len(buffer)) add_start := len(buffer.add) text_bytes := transmute([]u8)text for b in text_bytes { append(&buffer.add, b) } new_piece := Piece{.Add, add_start, len(text_bytes)} new_pieces: [dynamic]Piece inserted := false cursor := 0 for piece in buffer.pieces { piece_end := cursor + piece.len if !inserted && insert_at <= piece_end { inner := insert_at - cursor if inner > 0 { append(&new_pieces, Piece{piece.source, piece.start, inner}) } append(&new_pieces, new_piece) if inner < piece.len { append(&new_pieces, Piece{piece.source, piece.start + inner, piece.len - inner}) } inserted = true } else { append(&new_pieces, piece) } cursor = piece_end } if !inserted { append(&new_pieces, new_piece) } delete(buffer.pieces) buffer.pieces = new_pieces if bump_version do buffer.version += 1 buffer.line_index.dirty = true } buffer_delete_range :: proc(buffer: ^Buffer, offset: int, count: int) { if count <= 0 do return start := clamp_int(offset, 0, buffer_len(buffer)) end := clamp_int(start + count, start, buffer_len(buffer)) if start == end do return deleted := buffer_range_bytes(buffer, start, end - start) buffer_push_edit(&buffer.undo, Buffer_Edit{kind = .Delete, offset = start, text = deleted}) buffer_clear_edits(&buffer.redo) buffer_delete_range_raw(buffer, start, end - start) } buffer_replace_range :: proc(buffer: ^Buffer, offset: int, count: int, replacement: string) { start := clamp_int(offset, 0, buffer_len(buffer)) end := clamp_int(start + max_int(count, 0), start, buffer_len(buffer)) if start == end && len(replacement) == 0 do return deleted := buffer_range_bytes(buffer, start, end - start) if bytes_equal(deleted[:], transmute([]u8)replacement) { delete(deleted) return } replacement_bytes := clone_bytes(transmute([]u8)replacement) buffer_push_edit(&buffer.undo, Buffer_Edit{kind = .Replace, offset = start, text = deleted, replacement = replacement_bytes}) buffer_clear_edits(&buffer.redo) buffer_delete_range_raw_internal(buffer, start, end - start, false) buffer_insert_raw_internal(buffer, start, replacement, false) buffer.version += 1 buffer.line_index.dirty = true } buffer_delete_range_raw :: proc(buffer: ^Buffer, offset: int, count: int) { buffer_delete_range_raw_internal(buffer, offset, count, true) } buffer_delete_range_raw_internal :: proc(buffer: ^Buffer, offset: int, count: int, bump_version: bool) { if count <= 0 do return start := clamp_int(offset, 0, buffer_len(buffer)) end := clamp_int(start + count, start, buffer_len(buffer)) if start == end do return new_pieces: [dynamic]Piece cursor := 0 for piece in buffer.pieces { piece_start := cursor piece_end := cursor + piece.len if piece_end <= start || piece_start >= end { append(&new_pieces, piece) } else { keep_left := max_int(0, start - piece_start) keep_right := max_int(0, piece_end - end) if keep_left > 0 { append(&new_pieces, Piece{piece.source, piece.start, keep_left}) } if keep_right > 0 { right_start := piece.start + piece.len - keep_right append(&new_pieces, Piece{piece.source, right_start, keep_right}) } } cursor = piece_end } delete(buffer.pieces) buffer.pieces = new_pieces if bump_version do buffer.version += 1 buffer.line_index.dirty = true } buffer_undo :: proc(buffer: ^Buffer, cursor: ^Cursor) -> bool { edit, ok := buffer_pop_edit(&buffer.undo) if !ok do return false switch edit.kind { case .Insert: buffer_delete_range_raw(buffer, edit.offset, len(edit.text)) cursor.offset = edit.offset case .Delete: buffer_insert_raw(buffer, edit.offset, string(edit.text[:])) cursor.offset = edit.offset + len(edit.text) case .Replace: buffer_delete_range_raw_internal(buffer, edit.offset, len(edit.replacement), false) buffer_insert_raw_internal(buffer, edit.offset, string(edit.text[:]), false) buffer.version += 1 buffer.line_index.dirty = true cursor.offset = edit.offset + len(edit.text) } _, cursor.wanted_column = buffer_offset_to_line_col(buffer, cursor.offset) buffer_push_edit(&buffer.redo, edit) return true } buffer_redo :: proc(buffer: ^Buffer, cursor: ^Cursor) -> bool { edit, ok := buffer_pop_edit(&buffer.redo) if !ok do return false switch edit.kind { case .Insert: buffer_insert_raw(buffer, edit.offset, string(edit.text[:])) cursor.offset = edit.offset + len(edit.text) case .Delete: buffer_delete_range_raw(buffer, edit.offset, len(edit.text)) cursor.offset = edit.offset case .Replace: buffer_delete_range_raw_internal(buffer, edit.offset, len(edit.text), false) buffer_insert_raw_internal(buffer, edit.offset, string(edit.replacement[:]), false) buffer.version += 1 buffer.line_index.dirty = true cursor.offset = edit.offset + len(edit.replacement) } _, cursor.wanted_column = buffer_offset_to_line_col(buffer, cursor.offset) buffer_push_edit(&buffer.undo, edit) return true } buffer_bytes :: proc(buffer: ^Buffer) -> [dynamic]u8 { out: [dynamic]u8 for piece in buffer.pieces { switch piece.source { case .Original: chunk := buffer.original[piece.start:piece.start + piece.len] chunk_bytes := transmute([]u8)chunk for b in chunk_bytes { append(&out, b) } case .Add: for b in buffer.add[piece.start:piece.start + piece.len] { append(&out, b) } } } return out } buffer_rebuild_line_index :: proc(buffer: ^Buffer) { delete(buffer.line_index.starts) buffer.line_index.starts = make([dynamic]int) append(&buffer.line_index.starts, 0) text := buffer_bytes(buffer) defer delete(text) for b, i in text { if b == '\n' { append(&buffer.line_index.starts, i + 1) } } buffer.line_index.dirty = false } buffer_line_count :: proc(buffer: ^Buffer) -> int { if buffer.line_index.dirty { buffer_rebuild_line_index(buffer) } return len(buffer.line_index.starts) } buffer_line_start :: proc(buffer: ^Buffer, line: int) -> int { if buffer.line_index.dirty { buffer_rebuild_line_index(buffer) } if len(buffer.line_index.starts) == 0 do return 0 index := clamp_int(line, 0, len(buffer.line_index.starts) - 1) return buffer.line_index.starts[index] } buffer_line_end :: proc(buffer: ^Buffer, line: int) -> int { if buffer.line_index.dirty { buffer_rebuild_line_index(buffer) } line_count := len(buffer.line_index.starts) if line_count == 0 do return 0 index := clamp_int(line, 0, line_count - 1) if index + 1 < line_count { return max_int(buffer.line_index.starts[index], buffer.line_index.starts[index + 1] - 1) } return buffer_len(buffer) } buffer_offset_to_line_col :: proc(buffer: ^Buffer, offset: int) -> (line: int, column: int) { if buffer.line_index.dirty { buffer_rebuild_line_index(buffer) } target := clamp_int(offset, 0, buffer_len(buffer)) result := 0 for start, i in buffer.line_index.starts { if start > target do break result = i } return result, target - buffer.line_index.starts[result] } buffer_line_col_to_offset :: proc(buffer: ^Buffer, line: int, column: int) -> int { start := buffer_line_start(buffer, line) end := buffer_line_end(buffer, line) return clamp_int(start + max_int(column, 0), start, end) } buffer_line_bytes :: proc(buffer: ^Buffer, line: int) -> [dynamic]u8 { start := buffer_line_start(buffer, line) end := buffer_line_end(buffer, line) return buffer_range_bytes(buffer, start, end - start) } buffer_range_bytes :: proc(buffer: ^Buffer, offset: int, count: int) -> [dynamic]u8 { out: [dynamic]u8 if count <= 0 do return out start := clamp_int(offset, 0, buffer_len(buffer)) end := clamp_int(start + count, start, buffer_len(buffer)) cursor := 0 for piece in buffer.pieces { piece_start := cursor piece_end := cursor + piece.len if piece_end > start && piece_start < end { local_start := max_int(start - piece_start, 0) local_end := piece.len - max_int(piece_end - end, 0) switch piece.source { case .Original: chunk := buffer.original[piece.start + local_start:piece.start + local_end] chunk_bytes := transmute([]u8)chunk for b in chunk_bytes { append(&out, b) } case .Add: for b in buffer.add[piece.start + local_start:piece.start + local_end] { append(&out, b) } } } cursor = piece_end } return out } cursor_insert :: proc(buffer: ^Buffer, cursor: ^Cursor, text: string) { buffer_insert(buffer, cursor.offset, text) cursor.offset += len(text) _, cursor.wanted_column = buffer_offset_to_line_col(buffer, cursor.offset) } cursor_backspace :: proc(buffer: ^Buffer, cursor: ^Cursor) { if cursor.offset == 0 do return buffer_delete_range(buffer, cursor.offset - 1, 1) cursor.offset -= 1 _, cursor.wanted_column = buffer_offset_to_line_col(buffer, cursor.offset) } cursor_delete_forward :: proc(buffer: ^Buffer, cursor: ^Cursor) -> bool { if cursor.offset >= buffer_len(buffer) do return false buffer_delete_range(buffer, cursor.offset, 1) _, cursor.wanted_column = buffer_offset_to_line_col(buffer, cursor.offset) return true } cursor_insert_newline_auto_indent :: proc(buffer: ^Buffer, cursor: ^Cursor) { line, _ := buffer_offset_to_line_col(buffer, cursor.offset) line_bytes := buffer_line_bytes(buffer, line) defer delete(line_bytes) indent_len := 0 for b in line_bytes { if b == ' ' || b == '\t' { indent_len += 1 } else { break } } text: [dynamic]u8 defer delete(text) append(&text, '\n') for b in line_bytes[:indent_len] { append(&text, b) } cursor_insert(buffer, cursor, string(text[:])) } cursor_move_word_left :: proc(buffer: ^Buffer, cursor: ^Cursor) { text := buffer_bytes(buffer) defer delete(text) offset := clamp_int(cursor.offset, 0, len(text)) for offset > 0 && is_space_byte(text[offset - 1]) { offset -= 1 } if offset > 0 && is_identifier_byte(text[offset - 1]) { for offset > 0 && is_identifier_byte(text[offset - 1]) { offset -= 1 } } else if offset > 0 { offset -= 1 } cursor.offset = offset _, cursor.wanted_column = buffer_offset_to_line_col(buffer, cursor.offset) } cursor_move_word_right :: proc(buffer: ^Buffer, cursor: ^Cursor) { text := buffer_bytes(buffer) defer delete(text) offset := clamp_int(cursor.offset, 0, len(text)) if offset < len(text) && is_identifier_byte(text[offset]) { for offset < len(text) && is_identifier_byte(text[offset]) { offset += 1 } } else if offset < len(text) { offset += 1 } for offset < len(text) && is_space_byte(text[offset]) { offset += 1 } cursor.offset = offset _, cursor.wanted_column = buffer_offset_to_line_col(buffer, cursor.offset) } cursor_move_to_line_col :: proc(buffer: ^Buffer, cursor: ^Cursor, line: int, column: int) { cursor.offset = buffer_line_col_to_offset(buffer, line, column) _, cursor.wanted_column = buffer_offset_to_line_col(buffer, cursor.offset) } cursor_move_vertical :: proc(buffer: ^Buffer, cursor: ^Cursor, delta: int) { line, _ := buffer_offset_to_line_col(buffer, cursor.offset) target_line := clamp_int(line + delta, 0, buffer_line_count(buffer) - 1) cursor.offset = buffer_line_col_to_offset(buffer, target_line, cursor.wanted_column) } is_identifier_byte :: proc(b: u8) -> bool { return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9') || b == '_' } is_space_byte :: proc(b: u8) -> bool { return b == ' ' || b == '\t' || b == '\n' || b == '\r' } buffer_destroy :: proc(buffer: ^Buffer) { buffer_clear_edits(&buffer.undo) buffer_clear_edits(&buffer.redo) delete(buffer.add) delete(buffer.pieces) delete(buffer.line_index.starts) } buffer_push_edit :: proc(stack: ^[dynamic]Buffer_Edit, edit: Buffer_Edit) { append(stack, edit) } buffer_pop_edit :: proc(stack: ^[dynamic]Buffer_Edit) -> (edit: Buffer_Edit, ok: bool) { if len(stack^) == 0 do return {}, false index := len(stack^) - 1 edit = stack^[index] resize(stack, index) return edit, true } buffer_clear_edits :: proc(stack: ^[dynamic]Buffer_Edit) { for edit in stack^ { delete(edit.text) delete(edit.replacement) } clear(stack) } clone_bytes :: proc(bytes: []u8) -> [dynamic]u8 { out: [dynamic]u8 for b in bytes { append(&out, b) } return out } bytes_equal :: proc(a, b: []u8) -> bool { if len(a) != len(b) do return false for value, index in a { if value != b[index] do return false } return true } clamp_int :: proc(value, low, high: int) -> int { if value < low do return low if value > high do return high return value } max_int :: proc(a, b: int) -> int { if a > b do return a return b }