#define BUFFER_DEBUG SLOW_BUILD API bool AreEqual(XY a, XY b) { bool result = a.x == b.x && a.y == b.y; return result; } API Range MakeRange(Int a, Int b) { Range result = {Min(a, b), Max(a, b)}; return result; } API Range MakeRange(Int a) { Range result = {a, a}; return result; } API Int GetSize(Range range) { Assert(range.max >= range.min); Int result = range.max - range.min; return result; } API bool AreEqual(Range a, Range b) { bool result = a.min == b.min && a.max == b.max; return result; } API bool AreOverlapping(Range a, Range b) { bool r1 = (a.max >= b.min && a.max <= b.max) || (a.min >= b.min && a.min <= b.max); bool r2 = (b.max >= a.min && b.max <= a.max) || (b.min >= a.min && b.min <= a.max); return r1 || r2; } API bool InBounds(Range range, Int pos) { bool result = pos >= range.min && pos < range.max; return result; } API Range operator-(Range a, Int value) { a.min -= value; a.max -= value; return a; } API Range operator-=(Range &range, Int value) { range = range - value; return range; } API Range operator+(Range a, Int value) { a.min += value; a.max += value; return a; } API Range operator+=(Range &range, Int value) { range = range + value; return range; } API Range GetBufferEndAsRange(Buffer *buffer) { Range result = {buffer->len, buffer->len}; return result; } API Range GetRange(Buffer *buffer) { Range result = {0, buffer->len}; return result; } API Int Clamp(const Buffer *buffer, Int pos) { Int result = Clamp(pos, (Int)0, buffer->len); return result; } API Range Clamp(const Buffer *buffer, Range range) { Range result = {}; result.min = Clamp(buffer, range.min); result.max = Clamp(buffer, range.max); return result; } // Caret API Int GetFront(Caret caret) { Int result = caret.pos[caret.ifront]; return result; } API Int GetBack(Caret caret) { Int index = (caret.ifront + 1) % 2; Int result = caret.pos[index]; return result; } API Int GetMax(Caret caret) { Int result = Max(caret.pos[0], caret.pos[1]); return result; } API Int GetMin(Caret caret) { Int result = Min(caret.pos[0], caret.pos[1]); return result; } API Int GetSize(Caret caret) { return GetSize(caret.range); } API Caret MakeCaret(Int pos) { Caret result = {}; result.range.min = result.range.max = pos; return result; } API Caret MakeCaret(Int front, Int back) { Caret result = {}; if (front >= back) { result.range.min = back; result.range.max = front; result.ifront = 1; } else { result.range.min = front; result.range.max = back; result.ifront = 0; } return result; } API Caret SetBack(Caret caret, Int back) { Int front = GetFront(caret); Caret result = MakeCaret(front, back); return result; } API Caret SetFront(Caret caret, Int front) { Int back = GetBack(caret); Caret result = MakeCaret(front, back); return result; } API Caret SetFrontWithAnchor(Caret caret, Caret anchor, Int p) { if (anchor.range.min > p) { caret = MakeCaret(p, anchor.range.max); } else if (anchor.range.max < p) { caret = MakeCaret(p, anchor.range.min); } else { caret = anchor; } return caret; } API bool AreEqual(Caret a, Caret b) { bool result = AreEqual(a.range, b.range) && a.ifront == b.ifront; return result; } API bool AreOverlapping(Caret a, Caret b) { bool result = AreOverlapping(a.range, b.range); return result; } API Range GetLineRange(Buffer *buffer, Int line, Int *end_of_buffer = NULL) { Range result = {buffer->line_starts[line], buffer->len}; if (line + 1 < buffer->line_starts.len) { result.max = buffer->line_starts[line + 1]; } else if (end_of_buffer) { *end_of_buffer = 1; } return result; } API Range GetLineRangeWithoutNL(Buffer *buffer, Int line) { Int end_of_buffer = 0; Range line_range = GetLineRange(buffer, line, &end_of_buffer); line_range.max = line_range.max - 1 + end_of_buffer; return line_range; } API String16 GetString(Buffer *buffer, Range range = {0, INT64_MAX}) { range.min = Clamp(range.min, (Int)0, buffer->len); range.max = Clamp(range.max, (Int)0, buffer->len); String16 result = {(char16_t *)buffer->data + range.min, GetSize(range)}; return result; } API String AllocCharString(Allocator allocator, Buffer *buffer, Range range = {0, INT64_MAX}) { String16 string16 = GetString(buffer, range); String result = ToString(allocator, string16); return result; } API XY XYLine(Int line) { XY result = {}; result.line = line; return result; } API char16_t GetChar(Buffer *buffer, Int pos) { if (pos >= 0 && pos < buffer->len) return buffer->str[pos]; return 0; } API bool InBounds(const Buffer *buffer, Int pos) { bool result = pos >= 0 && pos < buffer->len; return result; } API Int LastLine(Buffer *buffer) { Int result = buffer->line_starts.len - 1; return result; } API String16 GetLineString(Buffer *buffer, Int line, Int *end_of_buffer = NULL) { Range range = GetLineRange(buffer, line, end_of_buffer); String16 string = GetString(buffer, range); return string; } API String16 GetLineStringWithoutNL(Buffer *buffer, Int line) { Range range = GetLineRangeWithoutNL(buffer, line); String16 string = GetString(buffer, range); return string; } API Int PosToLine(Buffer *buffer, Int pos) { Add(&buffer->line_starts, buffer->len + 1); // -2 here because we use 2 indices and combine them into one line range so we // don't want to access last item since that would index past array. Int low = 0; Int high = buffer->line_starts.len - 2; Int result = 0; while (low <= high) { Int mid = low + (high - low) / 2; Range range = {buffer->line_starts[mid], buffer->line_starts[mid + 1]}; if (pos >= range.min && pos < range.max) { result = mid; break; } if (range.min < pos) { low = mid + 1; } else { high = mid - 1; } } Pop(&buffer->line_starts); return result; } API XY PosToXY(Buffer *buffer, Int pos) { Int line = PosToLine(buffer, pos); Range line_range = GetLineRange(buffer, line); Int col = pos - line_range.min; XY result = {col, line}; return result; } API Int XYToPos(Buffer *buffer, XY xy) { xy.line = Clamp(xy.line, (Int)0, buffer->line_starts.len - 1); Range line_range = GetLineRange(buffer, xy.line); Int pos = Clamp(xy.col + line_range.min, line_range.min, line_range.max); return pos; } API Int XYToPosWithoutNL(Buffer *buffer, XY xy) { xy.line = Clamp(xy.line, (Int)0, buffer->line_starts.len - 1); Int end_of_buffer = 0; Range line_range = GetLineRange(buffer, xy.line, &end_of_buffer); Int pos = Clamp(xy.col + line_range.min, line_range.min, line_range.max - 1 + end_of_buffer); return pos; } API Int XYToPosErrorOutOfBounds(Buffer *buffer, XY xy) { if (xy.line < 0 || xy.line >= buffer->line_starts.len) return -1; Range line_range = GetLineRange(buffer, xy.line); Int pos = xy.col + line_range.min; if (pos < line_range.min || pos > line_range.max) return -1; return pos; } API Int GetWordStart(Buffer *buffer, Int pos) { pos = Clamp(pos, (Int)0, buffer->len); for (Int i = pos - 1; i >= 0; i -= 1) { if (!IsWord(buffer->str[i])) break; pos = i; } return pos; } API Int GetWordEnd(Buffer *buffer, Int pos) { pos = Clamp(pos, (Int)0, buffer->len); for (Int i = pos;; i += 1) { pos = i; // this is because buffer end terminates the loop // too early and we cannot establish the proper range // semantics - proper max is one past last index if (!(i < buffer->len)) break; if (!IsWord(buffer->str[i])) break; } return pos; } API bool IsLoadWord(char16_t w) { bool result = w == u'-' || w == u'/' || w == u'\\' || w == u':' || w == u'$' || w == u'_' || w == u'.' || w == u'!' || w == u'@' || w == '{' || w == '}'; if (!result) { result = !(IsSymbol(w) || IsWhitespace(w)); } return result; } API Int GetLoadWordStart(Buffer *buffer, Int pos) { pos = Clamp(pos, (Int)0, buffer->len); for (Int i = pos - 1; i >= 0; i -= 1) { if (!IsLoadWord(buffer->str[i])) break; pos = i; } return pos; } API Int GetLoadWordEnd(Buffer *buffer, Int pos) { pos = Clamp(pos, (Int)0, buffer->len); for (Int i = pos;; i += 1) { pos = i; // this is because buffer end terminates the loop // too early and we cannot establish the proper range // semantics - proper max is one past last index if (!(i < buffer->len)) break; if (!IsLoadWord(buffer->str[i])) break; } return pos; } API Range EncloseLoadWord(Buffer *buffer, Int pos) { Range result = {GetLoadWordStart(buffer, pos), GetLoadWordEnd(buffer, pos)}; return result; } API Int GetNextWordEnd(Buffer *buffer, Int pos) { pos = Clamp(pos, (Int)0, buffer->len); char16_t prev = 0; for (Int i = pos;; i += 1) { pos = i; // this is because buffer end terminates the loop // too early and we cannot establish the proper range // semantics - proper max is one past last index if (!(i < buffer->len)) break; if (prev == u'\n' || (prev && prev != buffer->str[i]) || IsWord(buffer->str[i])) { break; } prev = buffer->str[i]; } Int result = prev == u'\n' ? pos : GetWordEnd(buffer, pos); return result; } API Int GetPrevWordStart(Buffer *buffer, Int pos) { pos = Clamp(pos, (Int)0, buffer->len); char16_t prev = 0; Int i = pos - 1; for (; i >= 0; i -= 1) { if (prev == u'\n' || (prev && prev != buffer->str[i]) || IsWord(buffer->str[i])) { break; } pos = i; prev = buffer->str[i]; } bool new_line = prev == u'\n'; Int result = new_line ? pos : GetWordStart(buffer, pos); return result; } enum CharClass { CharClass_Whitespace, CharClass_Symbol, CharClass_Word, CharClass_NewLine, }; CharClass GetCharClass(char16_t c) { if (c == '\n') { return CharClass_NewLine; } if (IsWhitespace(c)) { return CharClass_Whitespace; } if (c == '_') { return CharClass_Word; } if (IsSymbol(c)) { return CharClass_Symbol; } return CharClass_Word; } API Int OnCharClassBoundary_GetNextWordEnd(Buffer *buffer, Int pos) { Int i = Clamp(pos, (Int)0, buffer->len); if (i < buffer->len && GetCharClass(buffer->str[i]) == CharClass_Whitespace) { i += 1; } CharClass initial_char_class = GetCharClass(buffer->str[i]); while (i < buffer->len && GetCharClass(buffer->str[i]) == initial_char_class) { i += 1; } return i; } API Int OnCharClassBoundary_GetPrevWordStart(Buffer *buffer, Int pos) { Int i = Clamp(pos - 1, (Int)0, buffer->len); if (i >= 0 && GetCharClass(buffer->str[i]) == CharClass_Whitespace) { i -= 1; } CharClass initial_word_class = GetCharClass(buffer->str[i]); while (i >= 0 && GetCharClass(buffer->str[i]) == initial_word_class) { i -= 1; } return i + 1; } API Int GetLineStart(Buffer *buffer, Int pos) { pos = Clamp(pos, (Int)0, buffer->len); Int line = PosToLine(buffer, pos); Range range = GetLineRangeWithoutNL(buffer, line); return range.min; } API Int GetLineEnd(Buffer *buffer, Int pos) { pos = Clamp(pos, (Int)0, buffer->len); Int line = PosToLine(buffer, pos); Range range = GetLineRangeWithoutNL(buffer, line); return range.max; } API Int GetFullLineStart(Buffer *buffer, Int pos) { pos = Clamp(pos, (Int)0, buffer->len); Int line = PosToLine(buffer, pos); Range range = GetLineRange(buffer, line); return range.min; } API Int GetFullLineEnd(Buffer *buffer, Int pos, Int *eof = NULL) { pos = Clamp(pos, (Int)0, buffer->len); Int line = PosToLine(buffer, pos); Range range = GetLineRange(buffer, line, eof); return range.max; } API Int GetBufferEnd(Buffer *buffer) { return buffer->len; } API Int GetNextEmptyLineStart(Buffer *buffer, Int pos) { Int result = pos; Int next_line = PosToLine(buffer, pos) + 1; for (Int line = next_line; line < buffer->line_starts.len; line += 1) { Range line_range = GetLineRangeWithoutNL(buffer, line); result = line_range.min; bool whitespace_line = true; for (Int i = line_range.min; i < line_range.max; i += 1) { if (!IsWhitespace(buffer->str[i])) { whitespace_line = false; break; } } if (whitespace_line) break; } return result; } API Int GetPrevEmptyLineStart(Buffer *buffer, Int pos) { Int result = pos; Int next_line = PosToLine(buffer, pos) - 1; for (Int line = next_line; line >= 0; line -= 1) { Range line_range = GetLineRangeWithoutNL(buffer, line); result = line_range.min; bool whitespace_line = true; for (Int i = line_range.min; i < line_range.max; i += 1) { if (!IsWhitespace(buffer->str[i])) { whitespace_line = false; break; } } if (whitespace_line) break; } return result; } API Range EncloseWord(Buffer *buffer, Int pos) { Range result = {GetWordStart(buffer, pos), GetWordEnd(buffer, pos)}; return result; } API Int SkipSpaces(Buffer *buffer, Int seek) { for (; seek < buffer->len; seek += 1) { char16_t c = GetChar(buffer, seek); if (c != u' ') break; } return seek; } API Range EncloseLine(Buffer *buffer, Int pos) { Range result = {GetLineStart(buffer, pos), GetLineEnd(buffer, pos)}; return result; } API Range EncloseFullLine(Buffer *buffer, Int pos) { Range result = {GetFullLineStart(buffer, pos), GetFullLineEnd(buffer, pos)}; return result; } API Int OffsetByLine(Buffer *buffer, Int pos, Int line_offset) { XY xy = PosToXY(buffer, pos); Int result = XYToPosWithoutNL(buffer, {xy.col, xy.line + line_offset}); return result; } API Int GetNextChar(Buffer *buffer, Int pos) { Int result = Clamp(pos + 1, (Int)0, buffer->len); return result; } API Int GetPrevChar(Buffer *buffer, Int pos) { Int result = Clamp(pos - 1, (Int)0, buffer->len); return result; } API Int GetLineIndent(Buffer *buffer, Int line) { String16 string = GetLineStringWithoutNL(buffer, line); Int indent = 0; for (Int i = 0; i < string.len; i += 1) { if (IsWhitespace(string.data[i])) { indent += 1; } else { break; } } return indent; } API Int GetIndentAtPos(Buffer *buffer, Int pos) { Int line = PosToLine(buffer, pos); Int result = GetLineIndent(buffer, line); return result; } API Range GetIndentRangeAtPos(Buffer *buffer, Int pos) { Int line = PosToLine(buffer, pos); Int indent = GetLineIndent(buffer, line); Range range = GetLineRangeWithoutNL(buffer, line); return {range.min, range.min + indent}; } API Int FindRangeByPos(Array *ranges, Int pos) { // binary search Int low = 0; Int high = ranges->len - 1; Int result = -1; while (low <= high) { Int mid = low + (high - low) / 2; Range range = ranges->data[mid]; if (pos >= range.min && pos < range.max) { result = mid; break; } if (range.min < pos) { low = mid + 1; } else { high = mid - 1; } } return result; } /////////////////////////////// // Raw operations /////////////////////////////// void RawGrow(Buffer *buffer, Int change_size) { Int new_size = buffer->len + change_size; if (new_size > buffer->cap) { Allocator alo = buffer->line_starts.allocator; Int outside = new_size - buffer->cap; Int new_cap = (buffer->cap + outside) * 2; U16 *new_array = AllocArray(alo, U16, new_cap); MemoryCopy(new_array, buffer->data, buffer->len * sizeof(U16)); Dealloc(alo, buffer->data); buffer->cap = new_cap; buffer->data = new_array; } } void RawOffsetAllLinesForward(Buffer *buffer, Int line, Int *_offset) { Int offset = *_offset; *_offset = 0; if (offset == 0) return; for (Int i = line; i < buffer->line_starts.len; i += 1) { buffer->line_starts[i] += offset; } } void UpdateLines(Buffer *buffer, Range range, String16 string) { if (buffer->no_line_starts) return; ProfileFunction(); Array &ls = buffer->line_starts; Int min_line_number = PosToLine(buffer, range.min); Assert(min_line_number < ls.len); // Update lines remove { Int line_offset = 0; Int lines_to_remove = min_line_number + 1; Int lines_to_remove_count = 0; for (Int i = range.min; i < range.max; i += 1) { char16_t c = buffer->data[i]; if (c == '\n') { lines_to_remove_count += 1; } line_offset -= 1; } RemoveManyByIndex(&ls, lines_to_remove, lines_to_remove_count); RawOffsetAllLinesForward(buffer, min_line_number + 1, &line_offset); } // Update lines add Int line_offset = 0; Int nl = min_line_number + 1; for (Int i = 0; i < string.len; i += 1) { nl = min_line_number + 1; bool next_line_valid = nl < ls.len; if (string[i] == u'\n') { Int new_line_pos = range.min + i + 1; line_offset += 1; Insert(&ls, new_line_pos, nl); RawOffsetAllLinesForward(buffer, nl + 1, &line_offset); min_line_number = nl; } else if (next_line_valid) { line_offset += 1; } } RawOffsetAllLinesForward(buffer, nl, &line_offset); } void RawValidateLineStarts(Buffer *buffer) { if (buffer->no_line_starts) { return; } Int line = 0; for (Int i = 0; i < buffer->len; i += 1) { Int l = PosToLine(buffer, i); Assert(l == line); if (buffer->data[i] == u'\n') line += 1; } } // These don't handle history, just raw operations on buffer memory // TODO: MAYBE THEY SHOULD! API void RawReplaceText(Buffer *buffer, Range range, String16 string) { ProfileFunction(); Assert(range.max >= range.min); Assert(range.max >= 0 && range.max <= buffer->len); Assert(range.min >= 0 && range.min <= buffer->len); buffer->dirty = true; buffer->change_id += 1; Int size_to_remove = range.max - range.min; Int size_to_add = string.len; Int change_size = size_to_add - size_to_remove; Assert(change_size + buffer->len >= 0); RawGrow(buffer, change_size); Int range_size = range.max - range.min; U16 *begin_remove = buffer->data + range.min; U16 *end_remove = begin_remove + range_size; Int remain_len = buffer->len - (range.min + range_size); UpdateLines(buffer, range, string); U16 *begin_add = begin_remove; U16 *end_add = begin_add + string.len; MemoryMove(end_add, end_remove, remain_len * sizeof(U16)); MemoryCopy(begin_add, string.data, string.len * sizeof(U16)); buffer->len = buffer->len + change_size; #if BUFFER_DEBUG RawValidateLineStarts(buffer); #endif } API void RawAppend(Buffer *buffer, String16 string) { RawReplaceText(buffer, GetBufferEndAsRange(buffer), string); } API void RawAppend(Buffer *buffer, String string) { Scratch scratch(buffer->line_starts.allocator); RawAppend(buffer, ToString16(scratch, string)); } API void RawAppendf(Buffer *buffer, const char *fmt, ...) { Scratch scratch(buffer->line_starts.allocator); STRING_FORMAT(scratch, fmt, string); String16 string16 = ToString16(scratch, string); RawReplaceText(buffer, GetBufferEndAsRange(buffer), string16); } /////////////////////////////// // multicursor /////////////////////////////// inline bool MergeSortCompare(Caret *EntryA, Caret *EntryB) { bool result = EntryA->range.min > EntryB->range.min; return result; } inline bool MergeSortCompare(Edit *EntryA, Edit *EntryB) { bool result = EntryA->range.min > EntryB->range.min; return result; } template void MergeSort(int64_t Count, T *First, T *Temp) { // SortKey = range.min if (Count == 1) { // NOTE(casey): No work to do. } else if (Count == 2) { T *EntryA = First; T *EntryB = First + 1; if (MergeSortCompare(EntryA, EntryB)) { Swap(EntryA, EntryB); } } else { Int Half0 = Count / 2; Int Half1 = Count - Half0; Assert(Half0 >= 1); Assert(Half1 >= 1); T *InHalf0 = First; T *InHalf1 = First + Half0; T *End = First + Count; MergeSort(Half0, InHalf0, Temp); MergeSort(Half1, InHalf1, Temp); T *ReadHalf0 = InHalf0; T *ReadHalf1 = InHalf1; T *Out = Temp; for (Int Index = 0; Index < Count; ++Index) { if (ReadHalf0 == InHalf1) { *Out++ = *ReadHalf1++; } else if (ReadHalf1 == End) { *Out++ = *ReadHalf0++; } else if (!MergeSortCompare(ReadHalf0, ReadHalf1)) { *Out++ = *ReadHalf0++; } else { *Out++ = *ReadHalf1++; } } Assert(Out == (Temp + Count)); Assert(ReadHalf0 == InHalf1); Assert(ReadHalf1 == End); // TODO(casey): Not really necessary if we ping-pong for (Int Index = 0; Index < Count; ++Index) { First[Index] = Temp[Index]; } } } API void ApplyEditsMultiCursor(Buffer *buffer, Array edits) { ProfileFunction(); #if BUFFER_DEBUG if (buffer->no_line_starts == false) { Assert(buffer->line_starts.len); } Assert(edits.len); For(edits) { Assert(it.range.min >= 0); Assert(it.range.max >= it.range.min); Assert(it.range.max <= buffer->len); } // Make sure edit ranges don't overlap ForItem(it1, edits) { ForItem(it2, edits) { if (&it1 == &it2) continue; bool a2_inside = it2.range.min >= it1.range.min && it2.range.min < it1.range.max; Assert(!a2_inside); bool b2_inside = it2.range.max > it1.range.min && it2.range.max <= it1.range.max; Assert(!b2_inside); } } #endif // We need to sort from lowest to highest based on range.min Scratch scratch(buffer->line_starts.allocator); Array edits_copy = TightCopy(scratch, edits); if (edits.len > 1) { MergeSort(edits.len, edits_copy.data, edits.data); } edits = edits_copy; #if BUFFER_DEBUG for (int64_t i = 0; i < edits.len - 1; i += 1) { Assert(edits[i].range.min <= edits[i + 1].range.min); } #endif // @optimize: we can do all edits in one go with less memory copies probably // or something else entirely Int offset = 0; For(edits) { it.range.min += offset; it.range.max += offset; offset += it.string.len - GetSize(it.range); RawReplaceText(buffer, it.range, it.string); } } API void AddEdit(Array *e, Range range, String16 string) { Add(e, {range, string}); } /////////////////////////////// // multicursor + history /////////////////////////////// HistoryEntry *SaveHistoryBeforeMergeCursor(Buffer *buffer, Array *stack, Array &carets) { if (buffer->no_history) return NULL; HistoryEntry entry = {}; entry.time = GetTimeSeconds(); entry.carets = TightCopy(GetSystemAllocator(), carets); Add(stack, entry); return GetLast(*stack); } void SaveHistoryBeforeApplyEdits(Buffer *buffer, Array *stack, Array &edits) { ProfileFunction(); if (buffer->no_history) return; HistoryEntry *entry = GetLast(*stack); Allocator sys_allocator = GetSystemAllocator(); entry->edits = TightCopy(sys_allocator, edits); // Make reverse edits For(entry->edits) { Range new_range = {it.range.min, it.range.min + it.string.len}; String16 string = GetString(buffer, it.range); it.string = Copy16(sys_allocator, string); it.range = new_range; } Scratch scratch; Array temp_edits = TightCopy(scratch, entry->edits); // Fix reverse edits ForItem(edit, edits) { Int remove_size = GetSize(edit.range); Int insert_size = edit.string.len; Int offset = insert_size - remove_size; for (Int i = 0; i < entry->edits.len; i += 1) { Edit &new_edit = entry->edits.data[i]; Edit &old_edit = temp_edits.data[i]; if (old_edit.range.min > edit.range.min) { new_edit.range.min += offset; new_edit.range.max += offset; } } } } API void RedoEdit(Buffer *buffer, Array *carets) { ProfileFunction(); if (buffer->no_history) return; for (;buffer->redo_stack.len > 0;) { HistoryEntry entry = Pop(&buffer->redo_stack); HistoryEntry *e = SaveHistoryBeforeMergeCursor(buffer, &buffer->undo_stack, *carets); e->time = entry.time; SaveHistoryBeforeApplyEdits(buffer, &buffer->undo_stack, entry.edits); ApplyEditsMultiCursor(buffer, entry.edits); Dealloc(carets); *carets = entry.carets; Allocator sys_allocator = GetSystemAllocator(); For(entry.edits) Dealloc(sys_allocator, it.string.data); Dealloc(&entry.edits); if (buffer->redo_stack.len > 0) { HistoryEntry *next = GetLast(buffer->redo_stack); if ((next->time - entry.time) <= UndoMergeTime) { continue; } } break; } } API void UndoEdit(Buffer *buffer, Array *carets) { ProfileFunction(); if (buffer->no_history) return; static bool warning_reported; for (int i = 0;buffer->undo_stack.len > 0; i += 1) { HistoryEntry entry = Pop(&buffer->undo_stack); HistoryEntry *e = SaveHistoryBeforeMergeCursor(buffer, &buffer->redo_stack, *carets); e->time = entry.time; SaveHistoryBeforeApplyEdits(buffer, &buffer->redo_stack, entry.edits); ApplyEditsMultiCursor(buffer, entry.edits); Dealloc(carets); *carets = entry.carets; Allocator sys_allocator = GetSystemAllocator(); For(entry.edits) Dealloc(sys_allocator, it.string.data); Dealloc(&entry.edits); if (i > 1000 && !warning_reported) { ReportConsolef("WARNING: Undoing more then 1000 edits at once, optimizations is needed I think, too much memory usage?"); warning_reported = true; } if (buffer->undo_stack.len > 0) { HistoryEntry *next = GetLast(buffer->undo_stack); if ((entry.time - next->time) <= UndoMergeTime) { continue; } } break; } } API void DeallocHistoryEntries(Array *entries) { For(*entries) { Dealloc(&it.carets); ForItem(edit, it.edits) Dealloc(it.edits.allocator, edit.string.data); Dealloc(&it.edits); } entries->len = 0; } API void ResetHistory(Buffer *buffer) { DeallocHistoryEntries(&buffer->redo_stack); DeallocHistoryEntries(&buffer->undo_stack); } API void ResetBuffer(Buffer *buffer) { ResetHistory(buffer); buffer->change_id += 1; buffer->line_starts.len = 0; buffer->len = 0; if (!buffer->no_line_starts) { Add(&buffer->line_starts, (Int)0); } } void ClearRedoStack(Buffer *buffer) { DeallocHistoryEntries(&buffer->redo_stack); } API void DeallocHistoryArray(Array *entries) { DeallocHistoryEntries(entries); Dealloc(entries); } API Array BeginEdit(Allocator allocator, Buffer *buffer, Array &carets) { Assert(buffer->edit_phase == 0 || buffer->edit_phase == 1); if (buffer->edit_phase == 0) { buffer->edit_phase += 1; Assert(carets.len); SaveHistoryBeforeMergeCursor(buffer, &buffer->undo_stack, carets); ClearRedoStack(buffer); } Array result = {allocator}; return result; } // We can invoke SaveCaretHistoryBeforeBeginEdit(which is basically BeginEdit with // different name before caret altering commands to save caret history // and then call some editing command to edit which is not going to save carets API void SaveCaretHistoryBeforeBeginEdit(Buffer *buffer, Array &carets) { BeginEdit({}, buffer, carets); } API void AssertRanges(Array carets) { For(carets) { Assert(it.range.max >= it.range.min); } } // Adjusts caret copies after edit to make them not move after for example // a bar modification. Sometimes works, sometimes doesn't, depends, not an all solving tool. // For example in case of ReopenBuffer, when we select and replace entire buffer, it didn't quite work. API void AdjustCarets(Array edits, Array *carets) { Scratch scratch; Array new_carets = TightCopy(scratch, *carets); ForItem(edit, edits) { Int remove_size = GetSize(edit.range); Int insert_size = edit.string.len; Int offset = insert_size - remove_size; for (Int i = 0; i < carets->len; i += 1) { Caret &oldc = carets->data[i]; Caret &newc = new_carets.data[i]; if (oldc.range.min > edit.range.min) { newc.range.min += offset; newc.range.max += offset; } } } for (Int i = 0; i < carets->len; i += 1) carets->data[i] = new_carets[i]; } constexpr bool EndEdit_KillSelection = true; constexpr bool EndEdit_SkipFixingCaretsIWantToDoThatMyself = true; API void EndEdit(Buffer *buffer, Array *edits, Array *carets, bool kill_selection, bool skip_fixing_carets_user_will_do_that_himself = false) { ProfileFunction(); { Assert(buffer->edit_phase == 1); buffer->edit_phase += 1; if (edits->len) { SaveHistoryBeforeApplyEdits(buffer, &buffer->undo_stack, *edits); ApplyEditsMultiCursor(buffer, *edits); } else { HistoryEntry entry = Pop(&buffer->undo_stack); Dealloc(&entry.carets); Assert(entry.edits.len == 0); } } Assert(buffer->edit_phase == 2); buffer->edit_phase -= 2; if (edits->len == 0) { return; } #if BUFFER_DEBUG if (buffer->no_history == false) { HistoryEntry *entry = GetLast(buffer->undo_stack); Assert(entry->carets.len); Assert(entry->edits.len); for (Int i = 0; i < edits->len - 1; i += 1) { Assert(edits->data[i].range.min <= edits->data[i + 1].range.min); } } #endif if (skip_fixing_carets_user_will_do_that_himself == false) { // Adjust carets // this one also moves the carets forward if they are aligned with edit // Scratch scratch; Array new_carets = TightCopy(scratch, *carets); ForItem(edit, *edits) { Int remove_size = GetSize(edit.range); Int insert_size = edit.string.len; Int offset = insert_size - remove_size; for (Int i = 0; i < carets->len; i += 1) { Caret &old_cursor = carets->data[i]; Caret &new_cursor = new_carets.data[i]; if (old_cursor.range.min == edit.range.min) { new_cursor.range.min += insert_size; } else if (old_cursor.range.min > edit.range.min) { new_cursor.range.min += offset; } if (old_cursor.range.max == edit.range.max) { new_cursor.range.max += insert_size; } else if (old_cursor.range.max > edit.range.max) { new_cursor.range.max += offset; } Assert(new_cursor.range.max >= new_cursor.range.min); } } for (Int i = 0; i < carets->len; i += 1) { carets->data[i] = new_carets[i]; if (kill_selection) { carets->data[i].range.max = carets->data[i].range.min; } } } } // Merge carets that overlap, this needs to be handled before any edits to // make sure overlapping edits won't happen. // // mouse_selection_anchor is special case for mouse handling ! API void MergeCarets(Buffer *buffer, Array *carets) { ProfileFunction(); For(*carets) it.range = Clamp(buffer, it.range); Caret first_caret = carets->data[0]; Scratch scratch; Array c1 = TightCopy(scratch, *carets); if (carets->len > 1) MergeSort(carets->len, c1.data, carets->data); carets->len = 0; Int first_caret_index = 0; Add(carets, c1[0]); for (Int i = 1; i < c1.len; i += 1) { Caret &it = c1[i]; Caret *last = GetLast(*carets); if (AreOverlapping(*last, it)) { last->range.max = Max(last->range.max, it.range.max); } else { Add(carets, it); } if (AreEqual(it, first_caret)) first_caret_index = carets->len - 1; } Swap(&carets->data[first_caret_index], &carets->data[0]); } API void InitBuffer(Allocator allocator, Buffer *buffer, BufferID id = {}, String name = "", Int size = 4096) { buffer->id = id; buffer->name = name; buffer->cap = size; buffer->data = AllocArray(allocator, U16, buffer->cap); buffer->line_starts.allocator = allocator; buffer->undo_stack.allocator = allocator; buffer->redo_stack.allocator = allocator; if (!buffer->no_line_starts) { Add(&buffer->line_starts, (Int)0); } if (DebugTraceBufferInits) printf("InitBuffer %.*s %p\n", (int)name.len, name.data, buffer->data); } API void DeinitBuffer(Buffer *buffer) { Allocator allocator = buffer->line_starts.allocator; Dealloc(allocator, buffer->data); Dealloc(&buffer->commands); Dealloc(&buffer->line_starts); DeallocHistoryArray(&buffer->undo_stack); DeallocHistoryArray(&buffer->redo_stack); if (DebugTraceBufferInits) printf("DeinitBuffer %.*s %p\n", (int)buffer->name.len, buffer->name.data, buffer->data); } // Indexing starts from 0 not 1 because this routine creates also the zero buffer // which is the buffer that often is defaulted to in case of errors API Buffer *AllocBuffer(Allocator allocator, String name = "", Int size = 4096) { Buffer *buffer = AllocType(allocator, Buffer); buffer->id = {BufferIDs++, buffer}; buffer->name = Intern(&GlobalInternTable, name); InitBuffer(allocator, buffer, buffer->id, buffer->name, size); return buffer; } API void DeallocBuffer(Buffer *buffer) { DeinitBuffer(buffer); Dealloc(buffer->line_starts.allocator, buffer); } API Buffer *CreateScratchBuffer(Allocator allocator, Int size = 4096) { Buffer *result = AllocType(allocator, Buffer); result->no_history = true; result->no_line_starts = true; InitBuffer(allocator, result, {}, "*temp*", size); return result; } void RunBufferTest() { { Scratch scratch; Buffer buffer = {}; InitBuffer(scratch, &buffer); Assert(buffer.line_starts.len == 1); String16 test_string = u"Thing itself"; { RawReplaceText(&buffer, {}, test_string); Assert(buffer.cap == 4096); Assert(buffer.len == 12); String16 a = GetString(&buffer); Assert(a == test_string); Assert(buffer.line_starts.len == 1); Assert(buffer.line_starts[0] == 0); Assert(PosToLine(&buffer, 4) == 0); } { RawReplaceText(&buffer, {0, 5}, u""); Assert(buffer.cap == 4096); Assert(buffer.len == 12 - 5); String16 a = GetString(&buffer); Assert(a == u" itself"); Assert(buffer.line_starts.len == 1); Assert(buffer.line_starts[0] == 0); Assert(PosToLine(&buffer, 4) == 0); } { RawReplaceText(&buffer, GetBufferEndAsRange(&buffer), u" and"); Assert(buffer.cap == 4096); Assert(buffer.len == 12 - 5 + 4); String16 a = GetString(&buffer); Assert(a == u" itself and"); Assert(buffer.line_starts.len == 1); Assert(buffer.line_starts[0] == 0); Assert(PosToLine(&buffer, 4) == 0); } { RawReplaceText(&buffer, GetRange(&buffer), u""); Assert(buffer.cap == 4096); Assert(buffer.len == 0); String16 a = GetString(&buffer); Assert(a == u""); Assert(buffer.line_starts.len == 1); Assert(buffer.line_starts[0] == 0); } { RawReplaceText(&buffer, GetBufferEndAsRange(&buffer), u"Memes and other\nthings"); Assert(buffer.line_starts.len == 2); Assert(PosToLine(&buffer, 17) == 1); Assert(PosToLine(&buffer, 16) == 1); Assert(PosToLine(&buffer, 15) == 0); Assert(buffer.data[15] == L'\n'); Assert(buffer.data[16] == L't'); RawReplaceText(&buffer, {}, u"Things as is\nand stuff\n"); Assert(buffer.line_starts.len == 4); Assert(PosToLine(&buffer, 12) == 0); Assert(buffer.data[12] == L'\n'); Assert(PosToLine(&buffer, 13) == 1); Assert(PosToLine(&buffer, 21) == 1); Assert(PosToLine(&buffer, 22) == 1); Assert(buffer.data[22] == L'\n'); Assert(PosToLine(&buffer, 23) == 2); Assert(PosToLine(&buffer, 37) == 2); Assert(PosToLine(&buffer, 38) == 2); Assert(buffer.data[38] == L'\n'); Assert(PosToLine(&buffer, 39) == 3); Assert(buffer.data[39] == L't'); RawReplaceText(&buffer, {}, u"a"); Assert(buffer.line_starts.len == 4); Assert(PosToLine(&buffer, 13) == 0); Assert(PosToLine(&buffer, 14) == 1); } } { Scratch scratch; Buffer buffer = {}; InitBuffer(scratch, &buffer); RawReplaceText(&buffer, {}, u"Thing\nmeme"); Assert(PosToLine(&buffer, 5) == 0); Assert(PosToLine(&buffer, 6) == 1); RawReplaceText(&buffer, {0, 1}, u""); Assert(PosToLine(&buffer, 4) == 0); Assert(PosToLine(&buffer, 5) == 1); RawReplaceText(&buffer, {4, 5}, u""); Assert(buffer.line_starts.len == 1); RawValidateLineStarts(&buffer); } // Make sure all the line starts are properly created { Scratch scratch; Buffer buffer = {}; InitBuffer(scratch, &buffer); RawReplaceText(&buffer, {}, u"Thing\nmeme"); RawReplaceText(&buffer, {0, 5}, u"per\ncop"); Assert(buffer.line_starts.len == (Int)3); Assert(PosToLine(&buffer, 3) == 0); Assert(PosToLine(&buffer, 4) == 1); RawValidateLineStarts(&buffer); RawReplaceText(&buffer, {0, 8}, u"Thing\nmeme"); RawReplaceText(&buffer, {0, 3}, u"Thing\nmeme"); RawReplaceText(&buffer, {9, 13}, u"Thing\nmeme"); RawReplaceText(&buffer, {4, 5}, u"Thing\nmeme\n\n\n"); RawReplaceText(&buffer, {22, 23}, u"\n\n\nThing\nmeme\n\n\n"); RawReplaceText(&buffer, {22, 23}, u"\n\n\nThing\nmeme\n\n\n"); RawReplaceText(&buffer, {22, 23}, u"\n\n\nThing\nmeme\n\n\n"); RawReplaceText(&buffer, {22, 23}, u"\n\n\nThing\nmeme\n\n\n"); RawValidateLineStarts(&buffer); } // Basic case make sure no leaks { Buffer buffer = {}; InitBuffer(GetTrackingAllocator(), &buffer); RawReplaceText(&buffer, {}, u"Thing\nmeme"); RawReplaceText(&buffer, GetBufferEndAsRange(&buffer), u"\nnewThing"); DeinitBuffer(&buffer); } // Testing Edit API and making sure no leaks { Scratch scratch; Buffer buffer = {}; InitBuffer(GetTrackingAllocator(), &buffer); RawReplaceText(&buffer, {}, u"Testing\nthings"); Array carets = {scratch}; Add(&carets, MakeCaret(0,7)); Add(&carets, MakeCaret(8,9)); Add(&carets, MakeCaret(GetBufferEnd(&buffer))); Array edits = BeginEdit(scratch, &buffer, carets); MergeCarets(&buffer, &carets); AddEdit(&edits, {0, 7}, u"t"); AddEdit(&edits, {8, 9}, u"T"); AddEdit(&edits, GetBufferEndAsRange(&buffer), u"\nnewThing"); EndEdit(&buffer, &edits, &carets, EndEdit_KillSelection); String16 s = GetString(&buffer); Assert(s == u"t\nThings\nnewThing"); DeinitBuffer(&buffer); Assert(MemoryTrackingRecord.len == 0); } // Make sure no_history and no line_starts properly makes sure of these { Scratch scratch; Buffer buffer = {}; buffer.no_history = true; buffer.no_line_starts = true; InitBuffer(GetTrackingAllocator(), &buffer); RawReplaceText(&buffer, {}, u"Testing\nthings"); Array carets = {scratch}; Add(&carets, MakeCaret(0,7)); Add(&carets, MakeCaret(8,9)); Add(&carets, MakeCaret(GetBufferEnd(&buffer))); Array edits = BeginEdit(scratch, &buffer, carets); MergeCarets(&buffer, &carets); AddEdit(&edits, {0, 7}, u"t"); AddEdit(&edits, {8, 9}, u"T"); AddEdit(&edits, GetBufferEndAsRange(&buffer), u"\nnewThing"); EndEdit(&buffer, &edits, &carets, EndEdit_KillSelection); String16 s = GetString(&buffer); Assert(s == u"t\nThings\nnewThing"); Assert(buffer.line_starts.len == 0); Assert(buffer.line_starts.data == 0); Assert(buffer.redo_stack.len == 0); Assert(buffer.redo_stack.data == 0); Assert(buffer.undo_stack.len == 0); Assert(buffer.undo_stack.data == 0); DeinitBuffer(&buffer); Assert(MemoryTrackingRecord.len == 0); } } RegisterFunction(&TestFunctions, RunBufferTest); /////////////////////////////// // Management BufferID AllocBufferID(Buffer *buffer) { return {BufferIDs++, buffer}; } Buffer *GetBuffer(BufferID id, Buffer *default_buffer = Buffers[0]) { Int left = 0; Int right = Buffers.len - 1; Buffer *result = default_buffer; while (left <= right) { Int mid = left + (right - left) / 2; Buffer *it = Buffers[mid]; if (it->id == id) { result = it; break; } else if (it->id.id < id.id) { left = mid + 1; } else { right = mid - 1; } } return result; } Buffer *GetBuffer(String name, Buffer *default_buffer = Buffers[0]) { For (Buffers) { if (it->name == name) return it; } return default_buffer; } bool IsNull(Buffer *buffer) { return buffer->id.id == 0; } void Close(BufferID id) { Buffer *buffer = GetBuffer(id, NULL); if (buffer) { if (buffer->id.id == 0 || buffer->special) { return; } buffer->close = true; RunGCThisFrame = true; } } Buffer *CreateBuffer(Allocator allocator, String name, Int size = 4096) { Buffer *result = AllocBuffer(allocator, name, size); Add(&Buffers, result); return result; } String GetUniqueBufferName(String working_dir, String prepend_name, String extension = ".log") { Scratch scratch; String buffer_name = {}; for (int i = 1; i < 1000; i += 1) { buffer_name = Format(scratch, "%S/%S%d%S", working_dir, prepend_name, i, extension); buffer_name = GetAbsolutePath(scratch, buffer_name); Buffer *exists = GetBuffer(buffer_name, NULL); if (!exists && !FileExists(buffer_name)) { break; } } buffer_name = Intern(&GlobalInternTable, buffer_name); return buffer_name; } Int ConvertUTF8ToUTF16UnixLine(String string, char16_t *buffer, Int buffer_cap) { if (string.len == 0) { return 0; } Int buffer_len = 0; Assert(buffer_cap > string.len * 2); for (Int i = 0; i < string.len;) { if (string.data[i] == '\r') { i += 1; continue; } if (string.data[i] == '\t') { // @WARNING: DONT INCREASE THE SIZE CARELESSLY, WE NEED TO ADJUST BUFFER SIZE for (Int ii = 0; ii < 4; ii += 1) buffer[buffer_len++] = u' '; i += 1; continue; } uint32_t u32 = '?'; UTF32Result decode = UTF8ToUTF32((uint8_t *)(string.data + i), (int64_t)(string.len - i)); if (!decode.error) { i += decode.advance; u32 = decode.out_str; } else { i += 1; } UTF16Result encode = UTF32ToUTF16(u32); if (!encode.error) { for (int64_t encode_i = 0; encode_i < encode.len; encode_i += 1) { buffer[buffer_len++] = encode.out_str[encode_i]; Assert(buffer_len < buffer_cap); } } else { buffer[buffer_len++] = u'?'; } } return buffer_len; } String16 ToUnixString16(Allocator allocator, String string_) { Int cap = string_.len * 3; char16_t *string16_buffer = AllocArray(allocator, char16_t, cap); Int len = ConvertUTF8ToUTF16UnixLine(string_, string16_buffer, cap); String16 string = {string16_buffer, len}; return string; } // This function as name suggests tries to open a buffer, // there is no name resolution here, path should already be resolved etc. // // 1. It tries to find an already open buffer // 2. Returns a buffer if the file doesn't exist (even if a directory doesn't exist) // - This is a worry for later time, also we want to handle weird names and so on // 3. If file exists we read it, convert to utf16, tabs to spaces etc. // // We don't really care about opening buffers that don't have proper paths Buffer *BufferOpenFile(String path) { ProfileFunction(); Allocator sys_allocator = GetSystemAllocator(); Scratch scratch; path = GetAbsolutePath(scratch, path); Buffer *buffer = GetBuffer(path); if (!IsNull(buffer) || (IsNull(buffer) && buffer->name == path)) { return buffer; } if (!FileExists(path)) { buffer = CreateBuffer(sys_allocator, path); } else if (IsDir(path)) { buffer = CreateBuffer(sys_allocator, path); #if PLUGIN_DIRECTORY_NAVIGATION buffer->is_dir = true; buffer->temp = true; #endif } else { String string = ReadFile(scratch, path); buffer = CreateBuffer(sys_allocator, path, string.len * 4 + 4096); buffer->len = ConvertUTF8ToUTF16UnixLine(string, buffer->str, buffer->cap); buffer->file_mod_time = GetFileModTime(path); UpdateLines(buffer, {}, String16{(char16_t *)buffer->data, buffer->len}); } return buffer; } void ReopenBuffer(Buffer *buffer) { Scratch scratch; #if PLUGIN_DIRECTORY_NAVIGATION if (buffer->is_dir) { InsertDirectoryNavigation(buffer); return; } #endif String string = ReadFile(scratch, buffer->name); if (string.len == 0) { return; } String16 string16 = ToUnixString16(scratch, string); ReplaceWithoutMovingCarets(buffer, GetRange(buffer), string16); buffer->file_mod_time = GetFileModTime(buffer->name); buffer->changed_on_disk = false; buffer->dirty = false; } void SaveBuffer(Buffer *buffer) { ProfileFunction(); if (TrimTrailingWhitespace) { TrimWhitespace(buffer, false); } Scratch scratch; String string = AllocCharString(scratch, buffer); bool success = WriteFile(buffer->name, string); if (success) { buffer->file_mod_time = GetFileModTime(buffer->name); buffer->dirty = false; buffer->temp = false; } else { ReportErrorf("Failed to save file with name: %S", buffer->name); } } void SaveAll() { For(Buffers) { // NOTE: file_mod_time is only set when buffer got read or written to disk already so should be saved if (it->file_mod_time && it->dirty) { SaveBuffer(it); } } } String GetDirectory(Buffer *buffer) { #if PLUGIN_DIRECTORY_NAVIGATION if (buffer->is_dir) { return buffer->name; } #endif String result = ChopLastSlash(buffer->name); return result; } void TryReopeningWhenModified(Buffer *it) { if (it->file_mod_time) { int64_t new_file_mod_time = GetFileModTime(it->name); if (it->file_mod_time != new_file_mod_time) { it->changed_on_disk = true; if (it->dirty == false) { ReopenBuffer(it); } } } }