1383 lines
42 KiB
C++
1383 lines
42 KiB
C++
#define BUFFER_DEBUG 0
|
|
|
|
|
|
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 GetBufferBeginAsRange(Buffer *buffer) {
|
|
Range result = {0, 0};
|
|
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 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) {
|
|
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) {
|
|
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) {
|
|
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) {
|
|
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);
|
|
|
|
// binary search
|
|
Int low = 0;
|
|
|
|
// -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 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 == u',';
|
|
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;
|
|
}
|
|
|
|
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) {
|
|
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 GetBufferStart(Buffer *buffer) {
|
|
return 0;
|
|
}
|
|
|
|
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 Int FindScopeEnd(Buffer *buffer, Int seek, Int max_seek, char16_t open, char16_t close) {
|
|
char16_t right = GetChar(buffer, seek);
|
|
if (right == open) {
|
|
int scope = 1;
|
|
Int i = seek + 1;
|
|
for (; i < seek + max_seek && i < buffer->len; i += 1) {
|
|
char16_t c = GetChar(buffer, i);
|
|
|
|
if (open == close && c == '\\') {
|
|
i += 1;
|
|
} else if (open == close && c == open) {
|
|
scope -= 1;
|
|
} else if (c == open) {
|
|
scope += 1;
|
|
} else if (c == close) {
|
|
scope -= 1;
|
|
}
|
|
|
|
if (c == u'\n' || scope == 0) break;
|
|
}
|
|
|
|
if (scope == 0) seek = i;
|
|
}
|
|
return seek;
|
|
}
|
|
|
|
API Range EncloseScope(Buffer *buffer, Int pos_min, Int pos_max, char16_t open, char16_t close) {
|
|
Range result = {pos_min, pos_max};
|
|
for (Int i = pos_min - 1; i >= 0; i -= 1) {
|
|
if (buffer->str[i] == open) {
|
|
result.min = i;
|
|
break;
|
|
}
|
|
}
|
|
for (Int i = pos_max; i < buffer->len; i += 1) {
|
|
if (buffer->str[i] == close) {
|
|
result.max = i + 1;
|
|
break;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
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};
|
|
}
|
|
|
|
const Int FuzzyCloserWordBegin = 5;
|
|
const Int FuzzyConsecutiveMultiplier = 3;
|
|
API Int FuzzyRate(String16 string, String16 with) {
|
|
if (with.len == 0) return 0;
|
|
Int points = 0;
|
|
Int consecutive = 0;
|
|
Int with_i = 0;
|
|
for (Int i = 0; i < string.len; i++) {
|
|
if (string.data[i] == with[with_i]) {
|
|
Int closer_begin = ClampBottom((Int)0, FuzzyCloserWordBegin - i);
|
|
points += closer_begin;
|
|
consecutive++;
|
|
with_i += 1;
|
|
} else {
|
|
points += consecutive * FuzzyConsecutiveMultiplier;
|
|
consecutive = 0;
|
|
with_i = 0;
|
|
}
|
|
|
|
if (with_i >= with.len) with_i = 0;
|
|
}
|
|
points += consecutive * FuzzyConsecutiveMultiplier;
|
|
return points;
|
|
}
|
|
|
|
API Array<FuzzyPair> FuzzySearchLines(Allocator allocator, Buffer *buffer, Int line_min, Int line_max, String16 needle) {
|
|
if (line_min < 0 || line_min >= buffer->line_starts.len) return {};
|
|
if (line_max < 0 || line_min > buffer->line_starts.len) return {};
|
|
Array<FuzzyPair> ratings = {allocator};
|
|
for (Int i = line_min; i < line_max; i += 1) {
|
|
String16 s = GetLineStringWithoutNL(buffer, i);
|
|
Int rating = FuzzyRate(s, needle);
|
|
Add(&ratings, {i, rating});
|
|
}
|
|
|
|
// bubble sort
|
|
for (Int i = 0; i < ratings.len - 1; i++) {
|
|
for (Int j = 0; j < ratings.len - 1; j++) {
|
|
if (ratings[j].rating < ratings[j + 1].rating) {
|
|
FuzzyPair temp = ratings[j];
|
|
ratings[j] = ratings[j + 1];
|
|
ratings[j + 1] = temp;
|
|
}
|
|
}
|
|
}
|
|
|
|
return ratings;
|
|
}
|
|
|
|
API Int FindRangeByPos(Array<Range> *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<Int> &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) {
|
|
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;
|
|
}
|
|
}
|
|
|
|
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
|
|
///////////////////////////////
|
|
|
|
void MergeSort(int64_t Count, Caret *First, Caret *Temp) {
|
|
ProfileFunction();
|
|
// SortKey = range.min
|
|
if (Count == 1) {
|
|
// NOTE(casey): No work to do.
|
|
} else if (Count == 2) {
|
|
Caret *EntryA = First;
|
|
Caret *EntryB = First + 1;
|
|
if (EntryA->range.min > EntryB->range.min) {
|
|
Swap(EntryA, EntryB);
|
|
}
|
|
} else {
|
|
int64_t Half0 = Count / 2;
|
|
int64_t Half1 = Count - Half0;
|
|
|
|
Assert(Half0 >= 1);
|
|
Assert(Half1 >= 1);
|
|
|
|
Caret *InHalf0 = First;
|
|
Caret *InHalf1 = First + Half0;
|
|
Caret *End = First + Count;
|
|
|
|
MergeSort(Half0, InHalf0, Temp);
|
|
MergeSort(Half1, InHalf1, Temp);
|
|
|
|
Caret *ReadHalf0 = InHalf0;
|
|
Caret *ReadHalf1 = InHalf1;
|
|
|
|
Caret *Out = Temp;
|
|
for (int64_t Index = 0;
|
|
Index < Count;
|
|
++Index) {
|
|
if (ReadHalf0 == InHalf1) {
|
|
*Out++ = *ReadHalf1++;
|
|
} else if (ReadHalf1 == End) {
|
|
*Out++ = *ReadHalf0++;
|
|
} else if (ReadHalf0->range.min < ReadHalf1->range.min) {
|
|
*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 (int64_t Index = 0;
|
|
Index < Count;
|
|
++Index) {
|
|
First[Index] = Temp[Index];
|
|
}
|
|
}
|
|
}
|
|
|
|
void MergeSort(int64_t Count, Edit *First, Edit *Temp) {
|
|
ProfileFunction();
|
|
// SortKey = range.min
|
|
if (Count == 1) {
|
|
// NOTE(casey): No work to do.
|
|
} else if (Count == 2) {
|
|
Edit *EntryA = First;
|
|
Edit *EntryB = First + 1;
|
|
if (EntryA->range.min > EntryB->range.min) {
|
|
Swap(EntryA, EntryB);
|
|
}
|
|
} else {
|
|
int64_t Half0 = Count / 2;
|
|
int64_t Half1 = Count - Half0;
|
|
|
|
Assert(Half0 >= 1);
|
|
Assert(Half1 >= 1);
|
|
|
|
Edit *InHalf0 = First;
|
|
Edit *InHalf1 = First + Half0;
|
|
Edit *End = First + Count;
|
|
|
|
MergeSort(Half0, InHalf0, Temp);
|
|
MergeSort(Half1, InHalf1, Temp);
|
|
|
|
Edit *ReadHalf0 = InHalf0;
|
|
Edit *ReadHalf1 = InHalf1;
|
|
|
|
Edit *Out = Temp;
|
|
for (int64_t Index = 0;
|
|
Index < Count;
|
|
++Index) {
|
|
if (ReadHalf0 == InHalf1) {
|
|
*Out++ = *ReadHalf1++;
|
|
} else if (ReadHalf1 == End) {
|
|
*Out++ = *ReadHalf0++;
|
|
} else if (ReadHalf0->range.min < ReadHalf1->range.min) {
|
|
*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 (int64_t Index = 0;
|
|
Index < Count;
|
|
++Index) {
|
|
First[Index] = Temp[Index];
|
|
}
|
|
}
|
|
}
|
|
|
|
API void ApplyEditsMultiCursor(Buffer *buffer, Array<Edit> edits) {
|
|
ProfileFunction();
|
|
#if BUFFER_DEBUG
|
|
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<Edit> 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<Edit> *e, Range range, String16 string) {
|
|
Add(e, {range, string});
|
|
}
|
|
|
|
///////////////////////////////
|
|
// multicursor + history
|
|
///////////////////////////////
|
|
|
|
void SaveHistoryBeforeMergeCursor(Buffer *buffer, Array<HistoryEntry> *stack, Array<Caret> &carets) {
|
|
if (buffer->no_history) return;
|
|
HistoryEntry entry = {};
|
|
entry.carets = TightCopy(GetSystemAllocator(), carets);
|
|
Add(stack, entry);
|
|
}
|
|
|
|
void SaveHistoryBeforeApplyEdits(Buffer *buffer, Array<HistoryEntry> *stack, Array<Edit> &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<Edit> 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<Caret> *carets) {
|
|
ProfileFunction();
|
|
if (buffer->no_history) return;
|
|
if (buffer->redo_stack.len <= 0) return;
|
|
|
|
HistoryEntry entry = Pop(&buffer->redo_stack);
|
|
|
|
SaveHistoryBeforeMergeCursor(buffer, &buffer->undo_stack, *carets);
|
|
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);
|
|
}
|
|
|
|
API void UndoEdit(Buffer *buffer, Array<Caret> *carets) {
|
|
ProfileFunction();
|
|
if (buffer->no_history) return;
|
|
if (buffer->undo_stack.len <= 0) return;
|
|
|
|
HistoryEntry entry = Pop(&buffer->undo_stack);
|
|
|
|
SaveHistoryBeforeMergeCursor(buffer, &buffer->redo_stack, *carets);
|
|
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);
|
|
}
|
|
|
|
API void DeallocHistoryEntries(Array<HistoryEntry> *entries) {
|
|
For(*entries) {
|
|
Dealloc(&it.carets);
|
|
ForItem(edit, it.edits) Dealloc(it.edits.allocator, edit.string.data);
|
|
Dealloc(&it.edits);
|
|
}
|
|
entries->len = 0;
|
|
}
|
|
|
|
void ClearRedoStack(Buffer *buffer) {
|
|
DeallocHistoryEntries(&buffer->redo_stack);
|
|
}
|
|
|
|
API void DeallocHistoryArray(Array<HistoryEntry> *entries) {
|
|
DeallocHistoryEntries(entries);
|
|
Dealloc(entries);
|
|
}
|
|
|
|
API Array<Edit> BeginEdit(Allocator allocator, Buffer *buffer, Array<Caret> &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<Edit> result = {allocator};
|
|
return result;
|
|
}
|
|
|
|
API void SaveCaretHistoryBeforeBeginEdit(Buffer *buffer, Array<Caret> &carets) {
|
|
BeginEdit({}, buffer, carets);
|
|
}
|
|
|
|
API void AssertRanges(Array<Caret> carets) {
|
|
For(carets) {
|
|
Assert(it.range.max >= it.range.min);
|
|
}
|
|
}
|
|
|
|
API void AdjustCarets(Array<Edit> edits, Array<Caret> *carets) {
|
|
Scratch scratch;
|
|
Array<Caret> 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];
|
|
}
|
|
|
|
API void EndEdit(Buffer *buffer, Array<Edit> *edits, Array<Caret> *carets, bool kill_selection) {
|
|
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 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
|
|
|
|
// Adjust carets
|
|
// this one also moves the carets forward if they are aligned with edit
|
|
//
|
|
Scratch scratch;
|
|
Array<Caret> 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<Caret> *carets) {
|
|
ProfileFunction();
|
|
For(*carets) it.range = Clamp(buffer, it.range);
|
|
Caret first_caret = carets->data[0];
|
|
|
|
Scratch scratch;
|
|
Array<Caret> 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);
|
|
}
|
|
}
|
|
|
|
API void DeinitBuffer(Buffer *buffer) {
|
|
Allocator allocator = buffer->line_starts.allocator;
|
|
Dealloc(allocator, buffer->data);
|
|
Dealloc(&buffer->line_starts);
|
|
DeallocHistoryArray(&buffer->undo_stack);
|
|
DeallocHistoryArray(&buffer->redo_stack);
|
|
}
|
|
|
|
// 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
|
|
Int BufferIDs;
|
|
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 *CreateTempBuffer(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, GetBufferBeginAsRange(&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, GetBufferBeginAsRange(&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
|
|
{
|
|
Scratch scratch;
|
|
Buffer buffer = {};
|
|
InitBuffer(GetTrackingAllocator(), &buffer);
|
|
RawReplaceText(&buffer, {}, u"Thing\nmeme");
|
|
RawReplaceText(&buffer, GetBufferEndAsRange(&buffer), u"\nnewThing");
|
|
DeinitBuffer(&buffer);
|
|
TrackingAllocatorCheck();
|
|
}
|
|
|
|
// Testing Edit API and making sure no leaks
|
|
{
|
|
Scratch scratch;
|
|
Buffer buffer = {};
|
|
InitBuffer(GetTrackingAllocator(), &buffer);
|
|
RawReplaceText(&buffer, {}, u"Testing\nthings");
|
|
Array<Caret> carets = {scratch};
|
|
Add(&carets, MakeCaret(0,7));
|
|
Add(&carets, MakeCaret(8,9));
|
|
Add(&carets, MakeCaret(GetBufferEnd(&buffer)));
|
|
Array<Edit> 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, KILL_SELECTION);
|
|
String16 s = GetString(&buffer);
|
|
Assert(s == u"t\nThings\nnewThing");
|
|
DeinitBuffer(&buffer);
|
|
TrackingAllocatorCheck();
|
|
}
|
|
|
|
// 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<Caret> carets = {scratch};
|
|
Add(&carets, MakeCaret(0,7));
|
|
Add(&carets, MakeCaret(8,9));
|
|
Add(&carets, MakeCaret(GetBufferEnd(&buffer)));
|
|
Array<Edit> 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, KILL_SELECTION);
|
|
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);
|
|
TrackingAllocatorCheck();
|
|
}
|
|
|
|
|
|
} |