Buffer :: struct { data: *u8; len: int; cap: int; lines: Lines; } Lines :: struct { data: *Range; len: int; cap: int; } Range :: struct { min: int; max: int; } LineInfo :: struct { number: int; range: Range; } GetRangeSize :: proc(range: Range): int { result := range.max - range.min; return result; } AdjustUTF8PosUnsafe :: proc(buffer: *Buffer, pos: int, direction: int = 1): int { for pos >= 0 && pos < buffer.len && IsUTF8ContinuationByte(buffer.data[pos]) { pos += direction; } return pos; } AdjustUTF8Pos :: proc(buffer: *Buffer, pos: int, direction: int = 1): int { assert(direction == 1 || direction == -1); pos = AdjustUTF8PosUnsafe(buffer, pos, direction); pos = ClampInt(pos, 0, buffer.len - 1); if (buffer.data) assert(!IsUTF8ContinuationByte(buffer.data[pos])); return pos; } AdjustRange :: proc(buffer: *Buffer, range: *Range) { range.min = AdjustUTF8Pos(buffer, range.min, direction = -1); range.max = AdjustUTF8Pos(buffer, range.max, direction = +1); } ReplaceText :: proc(buffer: *Buffer, replace: Range, with: String) { AdjustRange(buffer, &replace); new_buffer_len := buffer.len + :int(with.len) - GetRangeSize(replace); new_buffer_size := new_buffer_len + 1; // possible addition of a null terminator if new_buffer_size > buffer.cap { new_buffer_cap := MaxInt(4096, new_buffer_size * 2); new_buffer := malloc(:usize(new_buffer_cap)); if (buffer.data) { memcpy(new_buffer, buffer.data, :usize(buffer.len)); free(buffer.data); } buffer.data = new_buffer; buffer.cap = new_buffer_cap; } old_text_range: Range = replace; new_text_range: Range = {old_text_range.min, old_text_range.min + :int(with.len)}; right_range: Range = {old_text_range.max, buffer.len}; memmove(&buffer.data[new_text_range.max], &buffer.data[right_range.min], :usize(GetRangeSize(right_range))); memmove(&buffer.data[new_text_range.min], with.str, :usize(GetRangeSize(new_text_range))); buffer.len = new_buffer_len; if buffer.len && buffer.data[buffer.len - 1] != 0 { buffer.data[buffer.len] = 0; buffer.len += 1; assert(buffer.len <= buffer.cap); } UpdateBufferLines(buffer); } AddLine :: proc(lines: *Lines, line: Range) { if lines.len + 1 > lines.cap { new_cap := MaxInt(16, lines.cap * 2); lines.data = realloc(lines.data, :usize(new_cap) * sizeof(:Range)); lines.cap = new_cap; } lines.data[lines.len] = line; lines.len += 1; } UpdateBufferLines :: proc(buffer: *Buffer) { buffer.lines.len = 0; line: Range = {0, 0}; for i := 0; i < buffer.len; i += 1 { if buffer.data[i] == '\n' { AddLine(&buffer.lines, line); line.min = i + 1; line.max = i + 1; } else { line.max += 1; } } line.min = AdjustUTF8Pos(buffer, line.min); line.max = AdjustUTF8Pos(buffer, line.max); AddLine(&buffer.lines, line); } AllocStringFromBuffer :: proc(buffer: *Buffer, range: Range, null_terminate: bool = false): String { AdjustRange(buffer, &range); size := GetRangeSize(range); alloc_size := :usize(size); if (null_terminate) alloc_size += 1; data := malloc(alloc_size); memcpy(data, &buffer.data[range.min], :usize(size)); result: String = {data, size}; if (null_terminate) result.str[result.len] = 0; return result; } GetStringFromBuffer :: proc(buffer: *Buffer, range: Range): String { AdjustRange(buffer, &range); size := GetRangeSize(range); result: String = {:*char(&buffer.data[range.min]), size}; return result; } AddText :: proc(buffer: *Buffer, text: String) { end_of_buffer: Range = {buffer.len - 1, buffer.len - 1}; ReplaceText(buffer, end_of_buffer, text); } FindLineOfPos :: proc(buffer: *Buffer, pos: int): LineInfo { for i := 0; i < buffer.lines.len; i += 1 { line := buffer.lines.data[i]; if pos >= line.min && pos <= line.max { return {i, line}; } } return {}; } IsLineValid :: proc(buffer: *Buffer, line: int): bool { result := line >= 0 && line < buffer.lines.len; return result; } GetLine :: proc(buffer: *Buffer, line: int): LineInfo { line = ClampInt(line, 0, buffer.lines.len - 1); range := buffer.lines.data[line]; return {line, range}; } IsPosInBounds :: proc(buffer: *Buffer, pos: int): bool { result := buffer.len != 0 && pos >= 0 && pos < buffer.len; return result; } GetChar :: proc(buffer: *Buffer, pos: int): u8 { result: u8; if IsPosInBounds(buffer, pos) { result = buffer.data[pos]; } return result; } GetUTF32 :: proc(buffer: *Buffer, pos: int, codepoint_size: *int = nil): u32 { if !IsPosInBounds(buffer, pos) return 0; p := &buffer.data[pos]; max := buffer.len - pos; utf32 := UTF8ToUTF32(:*uchar(p), max); assert(utf32.error == 0); if (utf32.error != 0) return 0; if (codepoint_size) codepoint_size[0] = utf32.advance; return utf32.out_str; } IsWhitespace :: proc(w: u8): bool { result := w == '\n' || w == ' ' || w == '\t' || w == '\v' || w == '\r'; return result; } SeekOnWordBoundary :: proc(buffer: *Buffer, pos: int, direction: int = GO_FORWARD): int { assert(direction == GO_FORWARD || direction == GO_BACKWARD); check_pos: int; if direction == GO_FORWARD { check_pos = AdjustUTF8Pos(buffer, pos + 1, direction); pos = check_pos; // this difference here because the Backward for loop is not inclusive. // It doesn't move an inch forward after "pos" // - - - - pos - - // It goes backward on first Advance } else { check_pos = AdjustUTF8Pos(buffer, pos - 1, direction); } cursor_char := GetChar(buffer, check_pos); standing_on_whitespace := IsWhitespace(cursor_char); seek_whitespace := standing_on_whitespace == false; seek_word := standing_on_whitespace; end := buffer.len - 1; if (direction == GO_BACKWARD) end = 0; result := end; iter := Iterate(buffer, pos, end, direction); prev_pos := iter.pos; for IsValid(iter); Advance(&iter) { if seek_word && !IsWhitespace(:u8(iter.item)) { result = prev_pos; break; } if seek_whitespace && IsWhitespace(:u8(iter.item)) { if direction == GO_FORWARD { result = iter.pos; } else { result = prev_pos; } break; } prev_pos = iter.pos; } return result; }