Compare commits

...

7 Commits

12 changed files with 670 additions and 312 deletions

View File

@ -144,7 +144,7 @@ yank_whole_line :: proc(state: ^State) {
}
if buffer := current_buffer(state); buffer != nil {
selection := new_selection(buffer, buffer.cursor)
selection := new_selection(buffer, buffer.history.cursor)
length := selection_length(buffer, selection)
state.yank_register.whole_line = true

View File

@ -27,15 +27,10 @@ ContentSlice :: struct {
slice: []u8,
}
FileBufferIndex :: struct {
slice_index: int,
content_index: int,
}
Cursor :: struct {
col: int,
line: int,
index: FileBufferIndex,
index: PieceTableIndex,
}
Selection :: struct {
@ -51,13 +46,10 @@ FileBuffer :: struct {
extension: string,
top_line: int,
cursor: Cursor,
// cursor: Cursor,
selection: Maybe(Selection),
original_content: [dynamic]u8,
added_content: [dynamic]u8,
content_slices: [dynamic][]u8,
history: FileHistory,
glyphs: GlyphBuffer,
input_buffer: [dynamic]u8,
@ -66,14 +58,27 @@ FileBuffer :: struct {
FileBufferIter :: struct {
cursor: Cursor,
buffer: ^FileBuffer,
piter: PieceTableIter,
hit_end: bool,
}
// TODO: don't make this panic on nil snapshot
buffer_piece_table :: proc(file_buffer: ^FileBuffer) -> ^PieceTable {
return &file_buffer.history.piece_table
}
new_file_buffer_iter_from_beginning :: proc(file_buffer: ^FileBuffer) -> FileBufferIter {
return FileBufferIter { buffer = file_buffer };
return FileBufferIter {
buffer = file_buffer,
piter = new_piece_table_iter(buffer_piece_table(file_buffer))
};
}
new_file_buffer_iter_with_cursor :: proc(file_buffer: ^FileBuffer, cursor: Cursor) -> FileBufferIter {
return FileBufferIter { buffer = file_buffer, cursor = cursor };
return FileBufferIter {
buffer = file_buffer,
cursor = cursor,
piter = new_piece_table_iter_from_index(buffer_piece_table(file_buffer), cursor.index)
};
}
new_file_buffer_iter :: proc{new_file_buffer_iter_from_beginning, new_file_buffer_iter_with_cursor};
@ -81,89 +86,55 @@ file_buffer_end :: proc(buffer: ^FileBuffer) -> Cursor {
return Cursor {
col = 0,
line = 0,
index = FileBufferIndex {
slice_index = len(buffer.content_slices)-1,
content_index = len(buffer.content_slices[len(buffer.content_slices)-1])-1,
}
index = new_piece_table_index_from_end(buffer_piece_table(buffer))
};
}
iterate_file_buffer :: proc(it: ^FileBufferIter) -> (character: u8, idx: FileBufferIndex, cond: bool) {
if it.cursor.index.slice_index >= len(it.buffer.content_slices) || it.cursor.index.content_index >= len(it.buffer.content_slices[it.cursor.index.slice_index]) {
return;
}
cond = true;
iterate_file_buffer :: proc(it: ^FileBufferIter) -> (character: u8, idx: PieceTableIndex, cond: bool) {
character, idx, cond = iterate_piece_table_iter(&it.piter)
character = it.buffer.content_slices[it.cursor.index.slice_index][it.cursor.index.content_index];
if it.cursor.index.content_index < len(it.buffer.content_slices[it.cursor.index.slice_index])-1 {
it.cursor.index.content_index += 1;
} else if it.cursor.index.slice_index < len(it.buffer.content_slices)-1 {
it.cursor.index.content_index = 0;
it.cursor.index.slice_index += 1;
} else if it.hit_end {
return character, it.cursor.index, false;
} else {
it.hit_end = true;
return character, it.cursor.index, true;
}
it.cursor.index = it.piter.index
it.hit_end = it.piter.hit_end
if character == '\n' {
it.cursor.col = 0;
it.cursor.line += 1;
} else {
it.cursor.col += 1;
}
return character, it.cursor.index, true;
}
iterate_file_buffer_reverse_mangle_cursor :: proc(it: ^FileBufferIter) -> (character: u8, idx: FileBufferIndex, cond: bool) {
if len(it.buffer.content_slices[it.cursor.index.slice_index]) < 0 {
return character, idx, false;
}
character = it.buffer.content_slices[it.cursor.index.slice_index][it.cursor.index.content_index];
if it.cursor.index.content_index == 0 {
if it.cursor.index.slice_index > 0 {
it.cursor.index.slice_index -= 1;
it.cursor.index.content_index = len(it.buffer.content_slices[it.cursor.index.slice_index])-1;
} else if it.hit_end {
return character, it.cursor.index, false;
if cond && !it.hit_end {
if character == '\n' {
it.cursor.col = 0
it.cursor.line += 1
} else {
it.hit_end = true;
return character, it.cursor.index, true;
it.cursor.col += 1
}
} else {
it.cursor.index.content_index -= 1;
}
return character, it.cursor.index, true;
return
}
// TODO: figure out how to give the first character of the buffer
iterate_file_buffer_reverse :: proc(it: ^FileBufferIter) -> (character: u8, idx: FileBufferIndex, cond: bool) {
if character, idx, cond = iterate_file_buffer_reverse_mangle_cursor(it); cond {
if it.cursor.col < 1 {
if it.cursor.line > 0 {
line_length := file_buffer_line_length(it.buffer, it.cursor.index);
if line_length < 0 { line_length = 0; }
it.cursor.line -= 1;
it.cursor.col = line_length;
} else {
return character, it.cursor.index, false;
}
} else {
it.cursor.col -= 1;
}
// TODO: figure out how to give the first character of the buffer
iterate_file_buffer_reverse :: proc(it: ^FileBufferIter) -> (character: u8, idx: PieceTableIndex, cond: bool) {
character, idx, cond = iterate_piece_table_iter_reverse(&it.piter)
it.cursor.index = it.piter.index
it.hit_end = it.piter.hit_end
if cond && !it.hit_end {
if it.cursor.col > 0 {
it.cursor.col -= 1
} else if it.cursor.line > 0 {
line_length := file_buffer_line_length(it.buffer, it.cursor.index)
if line_length < 0 { line_length = 0 }
it.cursor.line -= 1
it.cursor.col = line_length
}
}
return character, it.cursor.index, cond;
return
}
get_character_at_iter :: proc(it: FileBufferIter) -> u8 {
return it.buffer.content_slices[it.cursor.index.slice_index][it.cursor.index.content_index];
return get_character_at_piece_table_index(buffer_piece_table(it.buffer), it.cursor.index);
}
IterProc :: proc(it: ^FileBufferIter) -> (character: u8, idx: FileBufferIndex, cond: bool);
IterProc :: proc(it: ^FileBufferIter) -> (character: u8, idx: PieceTableIndex, cond: bool);
UntilProc :: proc(it: ^FileBufferIter, iter_proc: IterProc) -> bool;
iterate_file_buffer_until :: proc(it: ^FileBufferIter, until_proc: UntilProc) {
@ -356,7 +327,7 @@ until_single_quote :: proc(it: ^FileBufferIter, iter_proc: IterProc) -> bool {
}
until_line_break :: proc(it: ^FileBufferIter, iter_proc: IterProc) -> (cond: bool) {
if it.buffer.content_slices[it.cursor.index.slice_index][it.cursor.index.content_index] == '\n' {
if get_character_at_piece_table_index(buffer_piece_table(it.buffer), it.cursor.index) == '\n' {
return false;
}
@ -372,7 +343,7 @@ update_file_buffer_index_from_cursor :: proc(buffer: ^FileBuffer) {
rendered_line := 0;
for character in iterate_file_buffer(&it) {
if line_length == buffer.cursor.col && rendered_line == buffer.cursor.line {
if line_length == buffer.history.cursor.col && rendered_line == buffer.history.cursor.line {
break;
}
@ -387,23 +358,23 @@ update_file_buffer_index_from_cursor :: proc(buffer: ^FileBuffer) {
}
// FIXME: just swap cursors
buffer.cursor.index = before_it.cursor.index;
buffer.history.cursor.index = before_it.cursor.index;
update_file_buffer_scroll(buffer);
}
file_buffer_line_length :: proc(buffer: ^FileBuffer, index: FileBufferIndex) -> int {
file_buffer_line_length :: proc(buffer: ^FileBuffer, index: PieceTableIndex) -> int {
line_length := 0;
if len(buffer.content_slices) <= 0 do return line_length
// if len(buffer.content_slices) <= 0 do return line_length
first_character := buffer.content_slices[index.slice_index][index.content_index];
first_character := get_character_at_piece_table_index(buffer_piece_table(buffer), index);
left_it := new_piece_table_iter_from_index(buffer_piece_table(buffer), index);
left_it := new_file_buffer_iter_with_cursor(buffer, Cursor { index = index });
if first_character == '\n' {
iterate_file_buffer_reverse_mangle_cursor(&left_it);
iterate_piece_table_iter_reverse(&left_it);
}
for character in iterate_file_buffer_reverse_mangle_cursor(&left_it) {
for character in iterate_piece_table_iter_reverse(&left_it) {
if character == '\n' {
break;
}
@ -411,9 +382,9 @@ file_buffer_line_length :: proc(buffer: ^FileBuffer, index: FileBufferIndex) ->
line_length += 1;
}
right_it := new_file_buffer_iter_with_cursor(buffer, Cursor { index = index });
right_it := new_piece_table_iter_from_index(buffer_piece_table(buffer), index);
first := true;
for character in iterate_file_buffer(&right_it) {
for character in iterate_piece_table_iter(&right_it) {
if character == '\n' {
break;
}
@ -431,7 +402,7 @@ move_cursor_start_of_line :: proc(buffer: ^FileBuffer, cursor: Maybe(^Cursor) =
cursor := cursor;
if cursor == nil {
cursor = &buffer.cursor;
cursor = &buffer.history.cursor;
}
if cursor.?.col > 0 {
@ -450,7 +421,7 @@ move_cursor_end_of_line :: proc(buffer: ^FileBuffer, stop_at_end: bool = true, c
cursor := cursor;
if cursor == nil {
cursor = &buffer.cursor;
cursor = &buffer.history.cursor;
}
it := new_file_buffer_iter_with_cursor(buffer, cursor.?^);
@ -474,7 +445,7 @@ move_cursor_up :: proc(buffer: ^FileBuffer, amount: int = 1, cursor: Maybe(^Curs
cursor := cursor;
if cursor == nil {
cursor = &buffer.cursor;
cursor = &buffer.history.cursor;
}
if cursor.?.line > 0 {
@ -508,7 +479,7 @@ move_cursor_down :: proc(buffer: ^FileBuffer, amount: int = 1, cursor: Maybe(^Cu
cursor := cursor;
if cursor == nil {
cursor = &buffer.cursor;
cursor = &buffer.history.cursor;
}
current_line := cursor.?.line;
@ -541,7 +512,7 @@ move_cursor_left :: proc(buffer: ^FileBuffer, cursor: Maybe(^Cursor) = nil) {
cursor := cursor;
if cursor == nil {
cursor = &buffer.cursor;
cursor = &buffer.history.cursor;
}
if cursor.?.col > 0 {
@ -555,7 +526,7 @@ move_cursor_right :: proc(buffer: ^FileBuffer, stop_at_end: bool = true, amt: in
cursor := cursor;
if cursor == nil {
cursor = &buffer.cursor;
cursor = &buffer.history.cursor;
}
it := new_file_buffer_iter_with_cursor(buffer, cursor.?^);
@ -573,7 +544,7 @@ move_cursor_forward_start_of_word :: proc(buffer: ^FileBuffer, cursor: Maybe(^Cu
cursor := cursor;
if cursor == nil {
cursor = &buffer.cursor;
cursor = &buffer.history.cursor;
}
it := new_file_buffer_iter_with_cursor(buffer, cursor.?^);
@ -587,7 +558,7 @@ move_cursor_forward_end_of_word :: proc(buffer: ^FileBuffer, cursor: Maybe(^Curs
cursor := cursor;
if cursor == nil {
cursor = &buffer.cursor;
cursor = &buffer.history.cursor;
}
it := new_file_buffer_iter_with_cursor(buffer, cursor.?^);
@ -601,7 +572,7 @@ move_cursor_backward_start_of_word :: proc(buffer: ^FileBuffer, cursor: Maybe(^C
cursor := cursor;
if cursor == nil {
cursor = &buffer.cursor;
cursor = &buffer.history.cursor;
}
it := new_file_buffer_iter_with_cursor(buffer, cursor.?^);
@ -616,7 +587,7 @@ move_cursor_backward_end_of_word :: proc(buffer: ^FileBuffer, cursor: Maybe(^Cur
cursor := cursor;
if cursor == nil {
cursor = &buffer.cursor;
cursor = &buffer.history.cursor;
}
it := new_file_buffer_iter_with_cursor(buffer, cursor.?^);
@ -666,10 +637,11 @@ swap_selections :: proc(selection: Selection) -> (swapped: Selection) {
return swapped
}
// TODO: don't access PieceTableIndex directly
is_selection_inverted :: proc(selection: Selection) -> bool {
return selection.start.index.slice_index > selection.end.index.slice_index ||
(selection.start.index.slice_index == selection.end.index.slice_index
&& selection.start.index.content_index > selection.end.index.content_index)
return selection.start.index.chunk_index > selection.end.index.chunk_index ||
(selection.start.index.chunk_index == selection.end.index.chunk_index
&& selection.start.index.char_index > selection.end.index.char_index)
}
selection_length :: proc(buffer: ^FileBuffer, selection: Selection) -> int {
@ -697,16 +669,12 @@ new_virtual_file_buffer :: proc(allocator: mem.Allocator) -> FileBuffer {
allocator = allocator,
file_path = "virtual_buffer",
original_content = slice.clone_to_dynamic([]u8{'\n'}),
added_content = make([dynamic]u8, 0, 1024*1024),
content_slices = make([dynamic][]u8, 0, 1024*1024),
history = make_history(),
glyphs = make_glyph_buffer(width, height),
input_buffer = make([dynamic]u8, 0, 1024),
};
append(&buffer.content_slices, buffer.original_content[:]);
return buffer;
}
@ -749,21 +717,12 @@ new_file_buffer :: proc(allocator: mem.Allocator, file_path: string, base_dir: s
// file_path = fi.fullpath[4:],
extension = extension,
original_content = slice.clone_to_dynamic(original_content),
added_content = make([dynamic]u8, 0, 1024*1024),
content_slices = make([dynamic][]u8, 0, 1024*1024),
history = make_history(original_content),
glyphs = make_glyph_buffer(width, height),
input_buffer = make([dynamic]u8, 0, 1024),
};
if len(buffer.original_content) > 0 {
append(&buffer.content_slices, buffer.original_content[:]);
} else {
append(&buffer.added_content, '\n')
append(&buffer.content_slices, buffer.added_content[:])
}
return buffer, error();
} else {
return FileBuffer{}, error(ErrorType.FileIOError, fmt.aprintf("failed to read from file"));
@ -775,10 +734,10 @@ save_buffer_to_disk :: proc(state: ^State, buffer: ^FileBuffer) -> (error: os.Er
defer os.close(fd);
offset: i64 = 0
for content_slice in buffer.content_slices {
os.write(fd, content_slice) or_return
for chunk in buffer_piece_table(buffer).chunks {
os.write(fd, chunk) or_return
offset += i64(len(content_slice))
offset += i64(len(chunk))
}
os.flush(fd)
@ -801,11 +760,9 @@ next_buffer :: proc(state: ^State, prev_buffer: ^int) -> int {
// TODO: replace this with arena for the file buffer
free_file_buffer :: proc(buffer: ^FileBuffer) {
delete(buffer.original_content);
delete(buffer.added_content);
delete(buffer.content_slices);
delete(buffer.glyphs.buffer);
delete(buffer.input_buffer);
free_history(&buffer.history)
delete(buffer.glyphs.buffer)
delete(buffer.input_buffer)
}
color_character :: proc(buffer: ^FileBuffer, start: Cursor, end: Cursor, palette_index: theme.PaletteColor) {
@ -854,8 +811,8 @@ draw_file_buffer :: proc(state: ^State, buffer: ^FileBuffer, x: int, y: int, sho
}
begin := buffer.top_line;
cursor_x := x + padding + buffer.cursor.col * state.source_font_width;
cursor_y := y + buffer.cursor.line * state.source_font_height;
cursor_x := x + padding + buffer.history.cursor.col * state.source_font_width;
cursor_y := y + buffer.history.cursor.line * state.source_font_height;
cursor_y -= begin * state.source_font_height;
@ -951,7 +908,7 @@ draw_file_buffer :: proc(state: ^State, buffer: ^FileBuffer, x: int, y: int, sho
update_file_buffer_scroll :: proc(buffer: ^FileBuffer, cursor: Maybe(^Cursor) = nil) {
cursor := cursor;
if cursor == nil {
cursor = &buffer.cursor;
cursor = &buffer.history.cursor;
}
if cursor.?.line > (buffer.top_line + buffer.glyphs.height - 5) {
@ -960,10 +917,10 @@ update_file_buffer_scroll :: proc(buffer: ^FileBuffer, cursor: Maybe(^Cursor) =
buffer.top_line = math.max(cursor.?.line - 5, 0);
}
// if buffer.cursor.line > (buffer.top_line + buffer.glyphs.height - 5) {
// buffer.top_line = math.max(buffer.cursor.line - buffer.glyphs.height + 5, 0);
// } else if buffer.cursor.line < (buffer.top_line + 5) {
// buffer.top_line = math.max(buffer.cursor.line - 5, 0);
// if buffer.history.cursor.line > (buffer.top_line + buffer.glyphs.height - 5) {
// buffer.top_line = math.max(buffer.history.cursor.line - buffer.glyphs.height + 5, 0);
// } else if buffer.history.cursor.line < (buffer.top_line + 5) {
// buffer.top_line = math.max(buffer.history.cursor.line - 5, 0);
// }
}
@ -987,28 +944,9 @@ insert_content :: proc(buffer: ^FileBuffer, to_be_inserted: []u8, append_to_end:
return;
}
// TODO: is this even needed? would mean that the cursor isn't always in a valid state.
// update_file_buffer_index_from_cursor(buffer);
it := new_file_buffer_iter_with_cursor(buffer, buffer.cursor) if !append_to_end else new_file_buffer_iter_with_cursor(buffer, file_buffer_end(buffer));
index := buffer.history.cursor.index if !append_to_end else new_piece_table_index_from_end(buffer_piece_table(buffer))
length := append(&buffer.added_content, ..to_be_inserted);
inserted_slice: []u8 = buffer.added_content[len(buffer.added_content)-length:];
if it.cursor.index.content_index == 0 {
// insertion happening in beginning of content slice
inject_at(&buffer.content_slices, it.cursor.index.slice_index, inserted_slice);
}
else {
// insertion is happening in middle of content slice
// cut current slice
end_slice := buffer.content_slices[it.cursor.index.slice_index][it.cursor.index.content_index:];
buffer.content_slices[it.cursor.index.slice_index] = buffer.content_slices[it.cursor.index.slice_index][:it.cursor.index.content_index];
inject_at(&buffer.content_slices, it.cursor.index.slice_index+1, inserted_slice);
inject_at(&buffer.content_slices, it.cursor.index.slice_index+2, end_slice);
}
insert_text(buffer_piece_table(buffer), to_be_inserted, buffer.history.cursor.index)
if !append_to_end {
update_file_buffer_index_from_cursor(buffer);
@ -1016,44 +954,6 @@ insert_content :: proc(buffer: ^FileBuffer, to_be_inserted: []u8, append_to_end:
}
}
// TODO: potentially add FileBufferIndex as parameter
split_content_slice_from_cursor :: proc(buffer: ^FileBuffer, cursor: ^Cursor) -> (did_split: bool) {
if cursor.index.content_index == 0 {
return;
}
end_slice := buffer.content_slices[cursor.index.slice_index][cursor.index.content_index:];
buffer.content_slices[cursor.index.slice_index] = buffer.content_slices[cursor.index.slice_index][:cursor.index.content_index];
inject_at(&buffer.content_slices, cursor.index.slice_index+1, end_slice);
// TODO: maybe move this out of this function
cursor.index.slice_index += 1;
cursor.index.content_index = 0;
return true
}
split_content_slice_from_selection :: proc(buffer: ^FileBuffer, selection: ^Selection) {
// TODO: swap selections
log.info("start:", selection.start, "- end:", selection.end);
// move the end cursor forward one (we want the splitting to be exclusive, not inclusive)
it := new_file_buffer_iter_with_cursor(buffer, selection.end);
iterate_file_buffer(&it);
selection.end = it.cursor;
split_content_slice_from_cursor(buffer, &selection.end);
if split_content_slice_from_cursor(buffer, &selection.start) {
selection.end.index.slice_index += 1;
}
log.info("start:", selection.start, "- end:", selection.end);
}
split_content_slice :: proc{split_content_slice_from_cursor, split_content_slice_from_selection};
delete_content_from_buffer_cursor :: proc(buffer: ^FileBuffer, amount: int) {
if amount <= len(buffer.input_buffer) {
runtime.resize(&buffer.input_buffer, len(buffer.input_buffer)-amount);
@ -1061,63 +961,22 @@ delete_content_from_buffer_cursor :: proc(buffer: ^FileBuffer, amount: int) {
amount := amount - len(buffer.input_buffer);
runtime.clear(&buffer.input_buffer);
if len(buffer.content_slices) < 1 {
return;
}
// Calculate proper line/col values
it := new_file_buffer_iter_with_cursor(buffer, buffer.history.cursor);
iterate_file_buffer_reverse(&it)
split_content_slice(buffer, &buffer.cursor);
delete_text(buffer_piece_table(buffer), &buffer.history.cursor.index)
it := new_file_buffer_iter_with_cursor(buffer, buffer.cursor);
// go back one (to be at the end of the content slice)
iterate_file_buffer_reverse(&it);
for i in 0..<amount {
content_slice_ptr := &buffer.content_slices[it.cursor.index.slice_index];
content_slice_len := len(content_slice_ptr^);
if content_slice_len == 1 {
// move cursor to previous content_slice so we can delete the current one
iterate_file_buffer_reverse(&it);
if it.hit_end {
runtime.ordered_remove(&buffer.content_slices, it.cursor.index.slice_index);
} else {
runtime.ordered_remove(&buffer.content_slices, it.cursor.index.slice_index+1);
}
} else if !it.hit_end {
iterate_file_buffer_reverse(&it);
content_slice_ptr^ = content_slice_ptr^[:len(content_slice_ptr^)-1];
}
}
if !it.hit_end {
iterate_file_buffer(&it);
}
buffer.cursor = it.cursor;
buffer.history.cursor.line = it.cursor.line
buffer.history.cursor.col = it.cursor.col
}
}
delete_content_from_selection :: proc(buffer: ^FileBuffer, selection: ^Selection) {
assert(len(buffer.content_slices) >= 1);
selection^ = swap_selections(selection^)
delete_text_in_span(buffer_piece_table(buffer), &selection.start.index, &selection.end.index)
split_content_slice(buffer, selection);
it := new_file_buffer_iter_with_cursor(buffer, selection.start);
// go back one (to be at the end of the content slice)
iterate_file_buffer_reverse(&it);
for _ in selection.start.index.slice_index..<selection.end.index.slice_index {
runtime.ordered_remove(&buffer.content_slices, selection.start.index.slice_index);
}
if !it.hit_end {
iterate_file_buffer(&it);
}
buffer.cursor = it.cursor;
buffer.history.cursor.index = selection.start.index
}
delete_content :: proc{delete_content_from_buffer_cursor, delete_content_from_selection};

View File

@ -43,7 +43,7 @@ update_glyph_buffer_from_file_buffer :: proc(buffer: ^FileBuffer) {
if rendered_line >= begin && screen_line >= buffer.glyphs.height { break; }
// render INSERT mode text into glyph buffer
if len(buffer.input_buffer) > 0 && rendered_line == buffer.cursor.line && rendered_col >= buffer.cursor.col && rendered_col < buffer.cursor.col + len(buffer.input_buffer) {
if len(buffer.input_buffer) > 0 && rendered_line == buffer.history.cursor.line && rendered_col >= buffer.history.cursor.col && rendered_col < buffer.history.cursor.col + len(buffer.input_buffer) {
for k in 0..<len(buffer.input_buffer) {
screen_line = rendered_line - begin;

135
src/core/history.odin Normal file
View File

@ -0,0 +1,135 @@
package core
import "core:log"
FileHistory :: struct {
piece_table: PieceTable,
cursor: Cursor,
snapshots: []Snapshot,
next: int,
first: int
}
Snapshot :: struct {
chunks: [dynamic][]u8,
cursor: Cursor,
}
make_history_with_data :: proc(initial_data: []u8, starting_capacity: int = 1024, allocator := context.allocator) -> FileHistory {
context.allocator = allocator
return FileHistory {
piece_table = make_piece_table(initial_data, starting_capacity = starting_capacity),
snapshots = make([]Snapshot, starting_capacity),
next = 0,
first = 0,
}
}
make_history_empty :: proc(starting_capacity: int = 1024, allocator := context.allocator) -> FileHistory {
context.allocator = allocator
return FileHistory {
piece_table = make_piece_table(starting_capacity = starting_capacity),
snapshots = make([]Snapshot, starting_capacity),
next = 0,
first = 0,
}
}
make_history :: proc{make_history_with_data, make_history_empty}
free_history :: proc(history: ^FileHistory) {
for snapshot in &history.snapshots {
if snapshot.chunks != nil {
delete(snapshot.chunks);
}
}
delete(history.snapshots)
delete(history.piece_table.original_content)
delete(history.piece_table.added_content)
delete(history.piece_table.chunks)
}
push_new_snapshot :: proc(history: ^FileHistory) {
if history.snapshots[history.next].chunks != nil {
delete(history.snapshots[history.next].chunks)
}
history.snapshots[history.next].chunks = clone_chunk(history.piece_table.chunks)
history.snapshots[history.next].cursor = history.cursor
history.next, history.first = next_indexes(history)
}
pop_snapshot :: proc(history: ^FileHistory, make_new_snapshot: bool = false) {
new_next, _ := next_indexes(history, backward = true)
if new_next == history.next do return
if make_new_snapshot {
push_new_snapshot(history)
}
history.next = new_next
delete(history.piece_table.chunks)
history.piece_table.chunks = clone_chunk(history.snapshots[history.next].chunks)
history.cursor = history.snapshots[history.next].cursor
}
recover_snapshot :: proc(history: ^FileHistory) {
new_next, _ := next_indexes(history)
if history.snapshots[new_next].chunks == nil do return
history.next = new_next
delete(history.piece_table.chunks)
history.piece_table.chunks = clone_chunk(history.snapshots[history.next].chunks)
history.cursor = history.snapshots[history.next].cursor
}
clone_chunk :: proc(chunks: [dynamic][]u8) -> [dynamic][]u8 {
new_chunks := make([dynamic][]u8, len(chunks), len(chunks))
for ptr, i in chunks {
new_chunks[i] = ptr
}
return new_chunks
}
next_indexes :: proc(history: ^FileHistory, backward: bool = false) -> (next: int, first: int) {
next = history.next
first = history.first
if backward {
if history.next == history.first {
return
}
next = history.next - 1
if next < 0 {
next = len(history.snapshots) - 1
}
} else {
next = history.next + 1
if next >= len(history.snapshots) {
next = 0
}
if next == first {
first += 1
}
if first >= len(history.snapshots) {
first = 0
}
}
return
}

241
src/core/piece_table.odin Normal file
View File

@ -0,0 +1,241 @@
package core
PieceTable :: struct {
original_content: []u8,
added_content: [dynamic]u8,
chunks: [dynamic][]u8,
}
PieceTableIter :: struct {
t: ^PieceTable,
index: PieceTableIndex,
hit_end: bool,
}
PieceTableIndex :: struct {
chunk_index: int,
char_index: int,
}
make_empty_piece_table :: proc(starting_capacity: int = 1024*1024, allocator := context.allocator) -> PieceTable {
context.allocator = allocator
original_content := transmute([]u8)string("\n")
chunks := make([dynamic][]u8, 0, starting_capacity)
append(&chunks, original_content[:])
return PieceTable {
original_content = original_content,
added_content = make([dynamic]u8, 0, starting_capacity),
chunks = chunks,
}
}
make_piece_table_from_bytes :: proc(data: []u8, starting_capacity: int = 1024*1024, allocator := context.allocator) -> PieceTable {
context.allocator = allocator
added_content := make([dynamic]u8, 0, starting_capacity)
chunks := make([dynamic][]u8, 0, starting_capacity)
if len(data) > 0 {
append(&chunks, data[:])
} else {
append(&added_content, '\n')
append(&chunks, added_content[:])
}
return PieceTable {
original_content = data,
added_content = added_content,
chunks = chunks,
}
}
make_piece_table :: proc{make_empty_piece_table, make_piece_table_from_bytes}
new_piece_table_iter :: proc(t: ^PieceTable) -> PieceTableIter {
return PieceTableIter {
t = t,
index = PieceTableIndex {
chunk_index = 0,
char_index = 0,
}
}
}
new_piece_table_iter_from_index :: proc(t: ^PieceTable, index: PieceTableIndex) -> PieceTableIter {
return PieceTableIter {
t = t,
index = index
}
}
new_piece_table_index_from_end :: proc(t: ^PieceTable) -> PieceTableIndex {
chunk_index := len(t.chunks)-1
char_index := len(t.chunks[chunk_index])-1
return PieceTableIndex {
chunk_index = chunk_index,
char_index = char_index,
}
}
iterate_piece_table_iter :: proc(it: ^PieceTableIter) -> (character: u8, index: PieceTableIndex, cond: bool) {
if it.index.chunk_index >= len(it.t.chunks) || it.index.char_index >= len(it.t.chunks[it.index.chunk_index]) {
return
}
character = it.t.chunks[it.index.chunk_index][it.index.char_index]
if it.hit_end {
return character, it.index, false
}
if it.index.char_index < len(it.t.chunks[it.index.chunk_index])-1 {
it.index.char_index += 1
} else if it.index.chunk_index < len(it.t.chunks)-1 {
it.index.char_index = 0
it.index.chunk_index += 1
} else {
it.hit_end = true
}
return character, it.index, true
}
iterate_piece_table_iter_reverse :: proc(it: ^PieceTableIter) -> (character: u8, index: PieceTableIndex, cond: bool) {
if it.index.chunk_index >= len(it.t.chunks) || it.index.char_index >= len(it.t.chunks[it.index.chunk_index]) {
return
}
character = it.t.chunks[it.index.chunk_index][it.index.char_index]
if it.hit_end {
return character, it.index, false
}
if it.index.char_index > 0 {
it.index.char_index -= 1
} else if it.index.chunk_index > 0 {
it.index.chunk_index -= 1
it.index.char_index = len(it.t.chunks[it.index.chunk_index])-1
} else {
it.hit_end = true
}
return character, it.index, true
}
get_character_at_piece_table_index :: proc(t: ^PieceTable, index: PieceTableIndex) -> u8 {
return t.chunks[index.chunk_index][index.char_index]
}
insert_text :: proc(t: ^PieceTable, to_be_inserted: []u8, index: PieceTableIndex) {
length := append(&t.added_content, ..to_be_inserted);
inserted_slice: []u8 = t.added_content[len(t.added_content)-length:];
if index.char_index == 0 {
// insertion happening in beginning of content slice
inject_at(&t.chunks, index.chunk_index, inserted_slice);
}
else {
// insertion is happening in middle of content slice
// cut current slice
end_slice := t.chunks[index.chunk_index][index.char_index:];
t.chunks[index.chunk_index] = t.chunks[index.chunk_index][:index.char_index];
inject_at(&t.chunks, index.chunk_index+1, inserted_slice);
inject_at(&t.chunks, index.chunk_index+2, end_slice);
}
}
delete_text :: proc(t: ^PieceTable, index: ^PieceTableIndex) {
if len(t.chunks) < 1 {
return;
}
split_from_index(t, index);
it := new_piece_table_iter_from_index(t, index^);
// go back one (to be at the end of the chunk)
iterate_piece_table_iter_reverse(&it);
chunk_ptr := &t.chunks[it.index.chunk_index];
chunk_len := len(chunk_ptr^);
if chunk_len == 1 {
// move cursor to previous chunk so we can delete the current one
iterate_piece_table_iter_reverse(&it);
if it.hit_end {
if len(t.chunks) > 1 {
ordered_remove(&t.chunks, it.index.chunk_index);
}
} else {
ordered_remove(&t.chunks, it.index.chunk_index+1);
}
} else if !it.hit_end {
iterate_piece_table_iter_reverse(&it);
chunk_ptr^ = chunk_ptr^[:len(chunk_ptr^)-1];
}
if !it.hit_end {
iterate_piece_table_iter(&it);
}
index^ = it.index
}
// Assumes end >= start
delete_text_in_span :: proc(t: ^PieceTable, start: ^PieceTableIndex, end: ^PieceTableIndex) {
assert(len(t.chunks) >= 1);
split_from_span(t, start, end);
it := new_piece_table_iter_from_index(t, start^);
// go back one (to be at the end of the content slice)
iterate_piece_table_iter_reverse(&it);
for _ in start.chunk_index..<end.chunk_index {
ordered_remove(&t.chunks, start.chunk_index);
}
if !it.hit_end {
iterate_piece_table_iter(&it);
}
start^ = it.index
end^ = it.index
}
split_from_index :: proc(t: ^PieceTable, index: ^PieceTableIndex) -> (did_split: bool) {
if index.char_index == 0 {
return;
}
end_slice := t.chunks[index.chunk_index][index.char_index:];
t.chunks[index.chunk_index] = t.chunks[index.chunk_index][:index.char_index];
inject_at(&t.chunks, index.chunk_index+1, end_slice);
index.chunk_index += 1;
index.char_index = 0;
return true
}
split_from_span :: proc(t: ^PieceTable, start: ^PieceTableIndex, end: ^PieceTableIndex) {
// move the end cursor forward one (we want the splitting to be exclusive, not inclusive)
it := new_piece_table_iter_from_index(t, end^);
iterate_piece_table_iter(&it);
end^ = it.index;
split_from_index(t, end);
if split_from_index(t, start) {
end.chunk_index += 1;
}
}

View File

@ -89,7 +89,7 @@ register_default_input_actions :: proc(input_map: ^core.InputActions) {
state.mode = .Visual;
core.reset_input_map(state)
core.current_buffer(state).selection = core.new_selection(core.current_buffer(state).cursor);
core.current_buffer(state).selection = core.new_selection(core.current_buffer(state).history.cursor);
}, "enter visual mode");
}
@ -158,6 +158,8 @@ register_default_visual_actions :: proc(input_map: ^core.InputActions) {
// Text Modification
{
core.register_key_action(input_map, .D, proc(state: ^State) {
core.push_new_snapshot(&core.current_buffer(state).history)
sel_cur := &(core.current_buffer(state).selection.?);
core.delete_content(core.current_buffer(state), sel_cur);
@ -169,6 +171,8 @@ register_default_visual_actions :: proc(input_map: ^core.InputActions) {
}, "delete selection");
core.register_key_action(input_map, .C, proc(state: ^State) {
core.push_new_snapshot(&core.current_buffer(state).history)
sel_cur := &(core.current_buffer(state).selection.?);
core.delete_content(core.current_buffer(state), sel_cur);
@ -194,6 +198,8 @@ register_default_visual_actions :: proc(input_map: ^core.InputActions) {
}, "Yank Line");
core.register_key_action(input_map, .P, proc(state: ^State) {
core.push_new_snapshot(&core.current_buffer(state).history)
if state.yank_register.whole_line {
core.insert_content(core.current_buffer(state), []u8{'\n'});
core.paste_register(state, state.yank_register)
@ -209,18 +215,32 @@ register_default_visual_actions :: proc(input_map: ^core.InputActions) {
register_default_text_input_actions :: proc(input_map: ^core.InputActions) {
core.register_key_action(input_map, .I, proc(state: ^State) {
core.push_new_snapshot(&core.current_buffer(state).history)
state.mode = .Insert;
sdl2.StartTextInput();
}, "enter insert mode");
core.register_key_action(input_map, .A, proc(state: ^State) {
core.push_new_snapshot(&core.current_buffer(state).history)
core.move_cursor_right(core.current_buffer(state), false);
state.mode = .Insert;
sdl2.StartTextInput();
}, "enter insert mode after character (append)");
core.register_key_action(input_map, .U, proc(state: ^State) {
core.pop_snapshot(&core.current_buffer(state).history, true)
}, "Undo");
core.register_ctrl_key_action(input_map, .R, proc(state: ^State) {
core.recover_snapshot(&core.current_buffer(state).history)
}, "Redo");
// TODO: add shift+o to insert newline above current one
core.register_key_action(input_map, .O, proc(state: ^State) {
core.push_new_snapshot(&core.current_buffer(state).history)
if buffer := core.current_buffer(state); buffer != nil {
core.move_cursor_end_of_line(buffer, false);
runtime.clear(&buffer.input_buffer)
@ -247,13 +267,17 @@ register_default_text_input_actions :: proc(input_map: ^core.InputActions) {
}
core.register_key_action(input_map, .P, proc(state: ^State) {
core.push_new_snapshot(&core.current_buffer(state).history)
if state.yank_register.whole_line {
core.move_cursor_end_of_line(core.current_buffer(state), false);
core.insert_content(core.current_buffer(state), []u8{'\n'});
core.move_cursor_right(core.current_buffer(state), false);
} else {
core.move_cursor_right(core.current_buffer(state))
}
core.paste_register(state, state.yank_register)
core.move_cursor_start_of_line(core.current_buffer(state))
core.reset_input_map(state)
}, "Paste");

View File

@ -389,6 +389,8 @@ main :: proc() {
sdl2.AddEventWatch(expose_event_watcher, &state);
core.push_new_snapshot(&core.current_buffer(&state).history)
control_key_pressed: bool;
for !state.should_close {
{

View File

@ -135,10 +135,10 @@ open_file_buffer_in_new_panel :: proc(state: ^core.State, file_path: string, lin
return;
}
buffer.cursor.line = line
buffer.cursor.col = col
buffer.history.cursor.line = line
buffer.history.cursor.col = col
buffer.top_line = buffer.history.cursor.line
core.update_file_buffer_index_from_cursor(&buffer)
core.update_file_buffer_scroll(&buffer)
buffer_index = len(state.buffers)
runtime.append(&state.buffers, buffer);
@ -183,15 +183,15 @@ render_file_buffer :: proc(state: ^core.State, s: ^ui.State, buffer: ^core.FileB
ui.open_element(s, nil, { kind = {ui.Grow{}, ui.Grow{}}})
ui.close_element(s)
it := core.new_file_buffer_iter_with_cursor(buffer, buffer.cursor)
it := core.new_file_buffer_iter_with_cursor(buffer, buffer.history.cursor)
ui.open_element(
s,
fmt.tprintf(
"%v:%v - Slice %v:%v - Char: %v",
buffer.cursor.line + 1,
buffer.cursor.col + 1,
buffer.cursor.index.slice_index,
buffer.cursor.index.content_index,
buffer.history.cursor.line + 1,
buffer.history.cursor.col + 1,
buffer.history.cursor.index.chunk_index,
buffer.history.cursor.index.char_index,
core.get_character_at_iter(it)
),
{}
@ -301,11 +301,13 @@ make_grep_panel :: proc(state: ^core.State) -> core.Panel {
free_grep_results(rs_results)
panel_state.selected_result = 0
core.update_glyph_buffer_from_bytes(
&panel_state.glyphs,
transmute([]u8)panel_state.query_results[panel_state.selected_result].file_context,
panel_state.query_results[panel_state.selected_result].line,
)
if len(panel_state.query_results) > 0 {
core.update_glyph_buffer_from_bytes(
&panel_state.glyphs,
transmute([]u8)panel_state.query_results[panel_state.selected_result].file_context,
panel_state.query_results[panel_state.selected_result].line,
)
}
}
core.register_key_action(&input_map.mode[.Normal], .ENTER, proc(state: ^core.State) {
@ -417,12 +419,19 @@ make_grep_panel :: proc(state: ^core.State) -> core.Panel {
{
if panel_state.query_results != nil {
// query results
ui.open_element(s, nil, {
query_result_container := ui.open_element(s, nil, {
dir = .TopToBottom,
kind = {ui.Grow{}, ui.Grow{}}
})
{
container_height := query_result_container.layout.size.y
max_results := container_height / 16
for result, i in panel_state.query_results {
if i > max_results {
break
}
ui.open_element(s, nil, {
dir = .LeftToRight,
kind = {ui.Fit{}, ui.Fit{}},
@ -448,6 +457,12 @@ make_grep_panel :: proc(state: ^core.State) -> core.Panel {
// file contents
selected_result := &panel_state.query_results[panel_state.selected_result]
core.update_glyph_buffer_from_bytes(
&panel_state.glyphs,
transmute([]u8)selected_result.file_context,
selected_result.line,
)
render_glyph_buffer(state, s, &panel_state.glyphs)
}
}

View File

@ -1,17 +1,12 @@
use std::{
error::Error,
ffi::{CStr, CString, OsString},
path::Path,
str::FromStr,
sync::mpsc::{Receiver, Sender},
thread,
ffi::CStr,
};
use grep::{
regex::RegexMatcherBuilder,
searcher::{BinaryDetection, SearcherBuilder, Sink, SinkError},
};
use std::sync::mpsc::channel;
use walkdir::WalkDir;
#[derive(Debug)]

View File

@ -32,15 +32,15 @@ buffer_to_string :: proc(buffer: ^core.FileBuffer) -> string {
}
length := 0
for content_slice in buffer.content_slices {
length += len(content_slice)
for chunk in core.buffer_piece_table(buffer).chunks {
length += len(chunk)
}
buffer_contents := make([]u8, length)
offset := 0
for content_slice in buffer.content_slices {
for c in content_slice {
for chunk in core.buffer_piece_table(buffer).chunks {
for c in chunk {
buffer_contents[offset] = c
offset += 1
}
@ -126,9 +126,9 @@ expect_line_col :: proc(t: ^testing.T, cursor: core.Cursor, line, col: int) {
testing.expect_value(t, cursor.col, col)
}
expect_cursor_index :: proc(t: ^testing.T, cursor: core.Cursor, slice_index, content_index: int) {
testing.expect_value(t, cursor.index.slice_index, slice_index)
testing.expect_value(t, cursor.index.content_index, content_index)
expect_cursor_index :: proc(t: ^testing.T, cursor: core.Cursor, chunk_index, char_index: int) {
testing.expect_value(t, cursor.index.chunk_index, chunk_index)
testing.expect_value(t, cursor.index.char_index, char_index)
}
@(test)
@ -142,8 +142,8 @@ insert_from_empty_no_newlines :: proc(t: ^testing.T) {
expected_text := fmt.aprintf("%v\n", inputted_text)
run_text_insertion(&e, inputted_text)
expect_line_col(t, buffer.cursor, 0, 12)
expect_cursor_index(t, buffer.cursor, 0, 12)
expect_line_col(t, buffer.history.cursor, 0, 12)
expect_cursor_index(t, buffer.history.cursor, 0, 12)
contents := buffer_to_string(core.current_buffer(&e))
testing.expectf(t, contents == expected_text, "got '%v', expected '%v'", contents, expected_text)
@ -160,8 +160,8 @@ insert_from_empty_with_newline :: proc(t: ^testing.T) {
expected_text := fmt.aprintf("%v\n", inputted_text)
run_text_insertion(&e, inputted_text)
expect_line_col(t, buffer.cursor, 1, 17)
expect_cursor_index(t, buffer.cursor, 0, 31)
expect_line_col(t, buffer.history.cursor, 1, 17)
expect_cursor_index(t, buffer.history.cursor, 0, 31)
contents := buffer_to_string(core.current_buffer(&e))
testing.expectf(t, contents == expected_text, "got '%v', expected '%v'", contents, expected_text)
@ -184,8 +184,8 @@ insert_in_between_text :: proc(t: ^testing.T) {
run_text_insertion(&e, " beautiful")
expect_line_col(t, buffer.cursor, 0, 15)
expect_cursor_index(t, buffer.cursor, 1, 9)
expect_line_col(t, buffer.history.cursor, 0, 15)
expect_cursor_index(t, buffer.history.cursor, 1, 9)
contents := buffer_to_string(core.current_buffer(&e))
testing.expectf(t, contents == expected_text, "got '%v', expected '%v'", contents, expected_text)
@ -211,8 +211,8 @@ insert_before_slice_at_beginning_of_file :: proc(t: ^testing.T) {
run_inputs(&e, []ArtificialInput{ press_key(.G), press_key(.H)})
run_text_insertion(&e, "Well, ")
expect_line_col(t, buffer.cursor, 0, 5)
expect_cursor_index(t, buffer.cursor, 0, 5)
expect_line_col(t, buffer.history.cursor, 0, 5)
expect_cursor_index(t, buffer.history.cursor, 0, 5)
contents := buffer_to_string(core.current_buffer(&e))
testing.expectf(t, contents == expected_text, "got '%v', expected '%v'", contents, expected_text)
@ -239,13 +239,46 @@ insert_before_slice :: proc(t: ^testing.T) {
run_text_insertion(&e, " rich")
expect_line_col(t, buffer.cursor, 0, 20)
expect_cursor_index(t, buffer.cursor, 2, 4)
expect_line_col(t, buffer.history.cursor, 0, 20)
expect_cursor_index(t, buffer.history.cursor, 2, 4)
contents := buffer_to_string(core.current_buffer(&e))
testing.expectf(t, contents == expected_text, "got '%v', expected '%v'", contents, expected_text)
}
@(test)
delete_last_content_slice_beginning_of_file :: proc(t: ^testing.T) {
e := new_test_editor()
setup_empty_buffer(&e)
buffer := &e.buffers[0]
run_text_insertion(&e, "Hello, world!")
// Delete just the text
run_input_multiple(&e, press_key(.I), 1)
run_input_multiple(&e, press_key(.BACKSPACE), 13)
expect_line_col(t, buffer.history.cursor, 0, 0)
expect_cursor_index(t, buffer.history.cursor, 0, 0)
// Try to delete when there is no text
run_input_multiple(&e, press_key(.BACKSPACE), 1)
expect_line_col(t, buffer.history.cursor, 0, 0)
expect_cursor_index(t, buffer.history.cursor, 0, 0)
testing.expect(t, len(core.buffer_piece_table(buffer).chunks) > 0, "BACKSPACE deleted final content slice in buffer")
// "commit" insert mode changes, then re-enter insert mode and try to delete again
run_input_multiple(&e, press_key(.ESCAPE), 1)
run_input_multiple(&e, press_key(.I), 1)
run_input_multiple(&e, press_key(.BACKSPACE), 1)
expect_line_col(t, buffer.history.cursor, 0, 0)
expect_cursor_index(t, buffer.history.cursor, 0, 0)
testing.expect(t, len(core.buffer_piece_table(buffer).chunks) > 0, "BACKSPACE deleted final content slice in buffer")
}
@(test)
delete_in_slice :: proc(t: ^testing.T) {
e := new_test_editor()
@ -273,8 +306,8 @@ delete_in_slice :: proc(t: ^testing.T) {
run_input_multiple(&e, press_key(.BACKSPACE), 3)
run_input_multiple(&e, press_key(.ESCAPE), 1)
expect_line_col(t, buffer.cursor, 0, 17)
expect_cursor_index(t, buffer.cursor, 3, 0)
expect_line_col(t, buffer.history.cursor, 0, 17)
expect_cursor_index(t, buffer.history.cursor, 3, 0)
contents := buffer_to_string(core.current_buffer(&e))
testing.expectf(t, contents == expected_text, "got '%v', expected '%v'", contents, expected_text)
@ -315,8 +348,8 @@ delete_across_slices :: proc(t: ^testing.T) {
run_input_multiple(&e, press_key(.BACKSPACE), 2)
run_input_multiple(&e, press_key(.ESCAPE), 1)
expect_line_col(t, buffer.cursor, 0, 16)
expect_cursor_index(t, buffer.cursor, 2, 0)
expect_line_col(t, buffer.history.cursor, 0, 16)
expect_cursor_index(t, buffer.history.cursor, 2, 0)
contents := buffer_to_string(core.current_buffer(&e))
testing.expectf(t, contents == expected_text, "got '%v', expected '%v'", contents, expected_text)
@ -342,8 +375,8 @@ move_down_next_line_has_shorter_length :: proc(t: ^testing.T) {
// Move down to the second line
run_input_multiple(&e, press_key(.J), 1)
expect_line_col(t, buffer.cursor, 1, 0)
expect_cursor_index(t, buffer.cursor, 0, 10)
expect_line_col(t, buffer.history.cursor, 1, 0)
expect_cursor_index(t, buffer.history.cursor, 0, 10)
}
@(test)
@ -361,8 +394,8 @@ move_down_on_last_line :: proc(t: ^testing.T) {
run_input_multiple(&e, press_key(.J), 1)
// Cursor should stay where it is
expect_line_col(t, buffer.cursor, 0, 8)
expect_cursor_index(t, buffer.cursor, 0, 8)
expect_line_col(t, buffer.history.cursor, 0, 8)
expect_cursor_index(t, buffer.history.cursor, 0, 8)
}
@(test)
@ -377,15 +410,15 @@ move_left_at_beginning_of_file :: proc(t: ^testing.T) {
// to ------------------^
run_input_multiple(&e, press_key(.H), 4)
expect_line_col(t, buffer.cursor, 0, 0)
expect_cursor_index(t, buffer.cursor, 0, 0)
expect_line_col(t, buffer.history.cursor, 0, 0)
expect_cursor_index(t, buffer.history.cursor, 0, 0)
// Try to move before the beginning of the file
run_input_multiple(&e, press_key(.H), 1)
// Should stay the same
expect_line_col(t, buffer.cursor, 0, 0)
expect_cursor_index(t, buffer.cursor, 0, 0)
expect_line_col(t, buffer.history.cursor, 0, 0)
expect_cursor_index(t, buffer.history.cursor, 0, 0)
}
@(test)
@ -399,15 +432,15 @@ move_right_at_end_of_file :: proc(t: ^testing.T) {
run_text_insertion(&e, "01234")
expect_line_col(t, buffer.cursor, 0, 4)
expect_cursor_index(t, buffer.cursor, 0, 4)
expect_line_col(t, buffer.history.cursor, 0, 4)
expect_cursor_index(t, buffer.history.cursor, 0, 4)
// Try to move after the end of the file
run_input_multiple(&e, press_key(.L), 1)
// Should stay the same
expect_line_col(t, buffer.cursor, 0, 4)
expect_cursor_index(t, buffer.cursor, 0, 4)
expect_line_col(t, buffer.history.cursor, 0, 4)
expect_cursor_index(t, buffer.history.cursor, 0, 4)
}
@(test)
@ -427,8 +460,8 @@ move_to_end_of_line_from_end :: proc(t: ^testing.T) {
// Move to the end of the line
run_inputs(&e, []ArtificialInput{ press_key(.G), press_key(.L)})
expect_line_col(t, buffer.cursor, 0, 4)
expect_cursor_index(t, buffer.cursor, 0, 4)
expect_line_col(t, buffer.history.cursor, 0, 4)
expect_cursor_index(t, buffer.history.cursor, 0, 4)
}
@(test)
@ -451,8 +484,8 @@ move_to_end_of_line_from_middle :: proc(t: ^testing.T) {
// Move to the end of the line
run_inputs(&e, []ArtificialInput{ press_key(.G), press_key(.L)})
expect_line_col(t, buffer.cursor, 0, 4)
expect_cursor_index(t, buffer.cursor, 0, 4)
expect_line_col(t, buffer.history.cursor, 0, 4)
expect_cursor_index(t, buffer.history.cursor, 0, 4)
}
@(test)
@ -475,8 +508,8 @@ move_to_beginning_of_line_from_middle :: proc(t: ^testing.T) {
// Move to the beginning of the line
run_inputs(&e, []ArtificialInput{ press_key(.G), press_key(.H)})
expect_line_col(t, buffer.cursor, 0, 0)
expect_cursor_index(t, buffer.cursor, 0, 0)
expect_line_col(t, buffer.history.cursor, 0, 0)
expect_cursor_index(t, buffer.history.cursor, 0, 0)
}
@(test)
@ -499,8 +532,30 @@ move_to_beginning_of_line_from_start :: proc(t: ^testing.T) {
// Move to the beginning of the line
run_inputs(&e, []ArtificialInput{ press_key(.G), press_key(.H)})
expect_line_col(t, buffer.cursor, 0, 0)
expect_cursor_index(t, buffer.cursor, 0, 0)
expect_line_col(t, buffer.history.cursor, 0, 0)
expect_cursor_index(t, buffer.history.cursor, 0, 0)
}
@(test)
append_end_of_line :: proc(t: ^testing.T) {
e := new_test_editor()
setup_empty_buffer(&e)
buffer := &e.buffers[0]
run_text_insertion(&e, "hello")
run_input_multiple(&e, press_key(.A), 1)
run_input_multiple(&e, press_key(.ESCAPE), 1)
expect_line_col(t, buffer.history.cursor, 0, 5)
expect_cursor_index(t, buffer.history.cursor, 1, 0)
run_input_multiple(&e, press_key(.A), 1)
run_input_multiple(&e, press_key(.ESCAPE), 1)
expect_line_col(t, buffer.history.cursor, 0, 5)
expect_cursor_index(t, buffer.history.cursor, 1, 0)
}
@(test)
@ -526,18 +581,44 @@ insert_line_under_current :: proc(t: ^testing.T) {
// Technically the cursor is still on the first line, because the `input_buffer`
// has been modified but not the actual contents of the filebuffer
expect_line_col(t, buffer.cursor, 0, 13)
expect_cursor_index(t, buffer.cursor, 0, 13)
expect_line_col(t, buffer.history.cursor, 0, 13)
expect_cursor_index(t, buffer.history.cursor, 0, 13)
run_text_insertion(&e, "This is the second line")
expect_line_col(t, buffer.cursor, 1, 22)
expect_cursor_index(t, buffer.cursor, 1, 23)
expect_line_col(t, buffer.history.cursor, 1, 22)
expect_cursor_index(t, buffer.history.cursor, 1, 23)
contents := buffer_to_string(core.current_buffer(&e))
testing.expectf(t, contents == expected_text, "got '%v', expected '%v'", contents, expected_text)
}
@(test)
yank_and_paste_whole_line :: proc(t: ^testing.T) {
e := new_test_editor()
setup_empty_buffer(&e)
buffer := &e.buffers[0]
initial_text := "Hello, world!\nThis is a new line"
run_text_insertion(&e, initial_text)
expected_text := "Hello, world!\nThis is a new line\nThis is a new line\n"
// Copy whole line
run_input_multiple(&e, press_key(.Y), 2)
// Move up to "Hello, world!"
run_input_multiple(&e, press_key(.K), 1)
// Paste it below current one
run_input_multiple(&e, press_key(.P), 1)
expect_line_col(t, buffer.history.cursor, 1, 0)
contents := buffer_to_string(core.current_buffer(&e))
testing.expectf(t, contents == expected_text, "got '%v', expected '%v'", contents, expected_text)
}
run_editor_frame :: proc(state: ^core.State, input: ArtificialInput, is_ctrl_pressed: ^bool) {
log.infof("running input: %v", input)

View File

@ -66,7 +66,7 @@ UI_Direction :: enum {
BottomToTop,
}
open_element :: proc(state: ^State, kind: UI_Element_Kind, layout: UI_Layout) {
open_element :: proc(state: ^State, kind: UI_Element_Kind, layout: UI_Layout) -> UI_Element {
e := UI_Element {
kind = kind,
layout = layout,
@ -93,6 +93,8 @@ open_element :: proc(state: ^State, kind: UI_Element_Kind, layout: UI_Layout) {
state.curr_elements[state.num_curr] = e
state.current_open_element = state.num_curr
state.num_curr += 1
return e
}
close_element :: proc(state: ^State, loc := #caller_location) -> UI_Layout {

10
todo.md
View File

@ -5,6 +5,8 @@
- Closing the only panel crashes
# Planned Features
- [ ] Highlight which panel is currently active
- [ ] Persist end of line cursor position
- Testing Harness
- [x] Replay user inputs and assert buffer contents/changes
- [ ] Finish writing tests for all current user actions
@ -31,7 +33,10 @@
- [x] Query across project
- [x] Open file in new buffer
- [x] Open file in new buffer at found location
- [ ] Preview file with context (instead of just the single matched line)
- [ ] Preview file with context
- [x] Show Context
- [ ] Properly show lines numbers
- [ ] Don't overlap result list with file preview
- [ ] Open Buffer Search
- Re-write the UI (again)
- [x] New UI
@ -45,7 +50,7 @@
- [x] Yank
- [x] Delete
- [ ] Change
- [ ] Change
- [x] Change
- [ ] Change word
- [ ] Change inside delimiter
- Virtual Whitespace
@ -53,4 +58,3 @@
- Command Search and Execution
- Refactor to remove generics added specifically for plugins
- Palette based UI?
- Persist end of line cursor position