test harness

memory-refactor
Patrick Cleaveliln 2025-07-10 06:24:05 +00:00
parent 8c352355de
commit 6b4d9f0cda
5 changed files with 637 additions and 10 deletions

View File

@ -8,3 +8,6 @@ editor: grep src/**/*.odin
grep:
cargo build --manifest-path "src/pkg/grep_lib/Cargo.toml"
test: src/**/*.odin
odin test src/tests/ -all-packages -debug -out:bin/test_runner

View File

@ -1073,7 +1073,7 @@ insert_content :: proc(buffer: ^FileBuffer, to_be_inserted: []u8, append_to_end:
if !append_to_end {
update_file_buffer_index_from_cursor(buffer);
move_cursor_right(buffer, false, amt = len(to_be_inserted));
move_cursor_right(buffer, false, amt = len(to_be_inserted) - 1);
}
}

View File

@ -320,7 +320,7 @@ main :: proc() {
} else {
buffer := core.new_virtual_file_buffer(context.allocator);
util.append_static_list(&state.panels, panels.make_file_buffer_panel(len(state.buffers)))
panels.open(&state, panels.make_file_buffer_panel(len(state.buffers)))
runtime.append(&state.buffers, buffer);
}

622
src/tests/tests.odin Normal file
View File

@ -0,0 +1,622 @@
package tests
import "base:runtime"
import "core:testing"
import "core:fmt"
import "core:mem"
import "core:log"
import "../core"
import "../panels"
import "../util"
new_test_editor :: proc() -> core.State {
state := core.State {
ctx = context,
screen_width = 640,
screen_height = 480,
source_font_width = 8,
source_font_height = 16,
panels = util.make_static_list(core.Panel, 128),
directory = "test_directory",
};
return state
}
buffer_to_string :: proc(buffer: ^core.FileBuffer) -> string {
if buffer == nil {
log.error("nil buffer")
}
length := 0
for content_slice in buffer.content_slices {
length += len(content_slice)
}
buffer_contents := make([]u8, length)
offset := 0
for content_slice in buffer.content_slices {
for c in content_slice {
buffer_contents[offset] = c
offset += 1
}
}
return string(buffer_contents)
}
ArtificialInput :: union {
ArtificialKey,
ArtificialTextInput,
}
ArtificialKey :: struct {
is_down: bool,
key: core.Key,
}
ArtificialTextInput :: struct {
text: string,
}
press_key :: proc(key: core.Key) -> ArtificialKey {
return ArtificialKey {
is_down = true,
key = key
}
}
release_key :: proc(key: core.Key) -> ArtificialKey {
return ArtificialKey {
is_down = false,
key = key
}
}
input_text :: proc(text: string) -> ArtificialTextInput {
return ArtificialTextInput {
text = text
}
}
setup_empty_buffer :: proc(state: ^core.State) {
buffer := core.new_virtual_file_buffer(context.allocator);
panels.open(state, panels.make_file_buffer_panel(len(state.buffers)))
runtime.append(&state.buffers, buffer);
core.reset_input_map(state)
}
run_inputs :: proc(state: ^core.State, inputs: []ArtificialInput) {
is_ctrl_pressed := false
for input in inputs {
run_editor_frame(state, input, &is_ctrl_pressed)
}
}
run_input_multiple :: proc(state: ^core.State, input: ArtificialInput, amount: int) {
is_ctrl_pressed := false
for _ in 0..<amount {
run_editor_frame(state, input, &is_ctrl_pressed)
}
}
run_text_insertion :: proc(state: ^core.State, text: string) {
is_ctrl_pressed := false
inputs := []ArtificialInput {
press_key(.I),
input_text(text),
press_key(.ESCAPE),
}
for input in inputs {
run_editor_frame(state, input, &is_ctrl_pressed)
}
}
expect_line_col :: proc(t: ^testing.T, cursor: core.Cursor, line, col: int) {
testing.expect_value(t, cursor.line, line)
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)
}
@(test)
insert_from_empty_no_newlines :: proc(t: ^testing.T) {
e := new_test_editor()
setup_empty_buffer(&e)
buffer := &e.buffers[0]
inputted_text := "Hello, world!"
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)
contents := buffer_to_string(core.current_buffer(&e))
testing.expectf(t, contents == expected_text, "got '%v', expected '%v'", contents, expected_text)
}
@(test)
insert_from_empty_with_newline :: proc(t: ^testing.T) {
e := new_test_editor()
setup_empty_buffer(&e)
buffer := &e.buffers[0]
inputted_text := "Hello, world!\nThis is a new line"
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)
contents := buffer_to_string(core.current_buffer(&e))
testing.expectf(t, contents == expected_text, "got '%v', expected '%v'", contents, expected_text)
}
@(test)
insert_in_between_text :: proc(t: ^testing.T) {
e := new_test_editor()
setup_empty_buffer(&e)
buffer := &e.buffers[0]
inputted_text := "Hello, world!"
expected_text := "Hello, beautiful world!\n"
run_text_insertion(&e, inputted_text)
// Move the cursor to the space in between 'Hello,' and 'world!'
run_input_multiple(&e, press_key(.H), 6)
run_text_insertion(&e, " beautiful")
expect_line_col(t, buffer.cursor, 0, 15)
expect_cursor_index(t, buffer.cursor, 1, 9)
contents := buffer_to_string(core.current_buffer(&e))
testing.expectf(t, contents == expected_text, "got '%v', expected '%v'", contents, expected_text)
}
@(test)
insert_before_slice_at_beginning_of_file :: proc(t: ^testing.T) {
e := new_test_editor()
setup_empty_buffer(&e)
buffer := &e.buffers[0]
inputted_text := "Hello, world!"
expected_text := "Well, Hello, beautiful world!\n"
run_text_insertion(&e, inputted_text)
// Move the cursor to the space in between 'Hello,' and 'world!'
run_input_multiple(&e, press_key(.H), 6)
run_text_insertion(&e, " beautiful")
// Move to beginning of line (and hence the file)
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)
contents := buffer_to_string(core.current_buffer(&e))
testing.expectf(t, contents == expected_text, "got '%v', expected '%v'", contents, expected_text)
}
@(test)
insert_before_slice :: proc(t: ^testing.T) {
e := new_test_editor()
setup_empty_buffer(&e)
buffer := &e.buffers[0]
inputted_text := "Hello, world!"
expected_text := "Hello, beautiful rich world!\n"
run_text_insertion(&e, inputted_text)
// Move the cursor to the space in between 'Hello,' and 'world!'
run_input_multiple(&e, press_key(.H), 6)
run_text_insertion(&e, " beautiful")
// Move right to the start of the slice of ' world!'
run_input_multiple(&e, press_key(.L), 1)
run_text_insertion(&e, " rich")
expect_line_col(t, buffer.cursor, 0, 20)
expect_cursor_index(t, buffer.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_in_slice :: proc(t: ^testing.T) {
e := new_test_editor()
setup_empty_buffer(&e)
buffer := &e.buffers[0]
inputted_text := "Hello, world!"
expected_text := "Hello, beautiful h world!\n"
// ------ - ---------
// ---------- -
// 0 1 234
run_text_insertion(&e, inputted_text)
// Move the cursor to the space in between 'Hello,' and 'world!'
run_input_multiple(&e, press_key(.H), 6)
run_text_insertion(&e, " beautiful")
// Move right to the start of the slice of ' world!'
run_input_multiple(&e, press_key(.L), 1)
run_text_insertion(&e, " rich")
run_input_multiple(&e, press_key(.I), 1)
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)
contents := buffer_to_string(core.current_buffer(&e))
testing.expectf(t, contents == expected_text, "got '%v', expected '%v'", contents, expected_text)
}
@(test)
delete_across_slices :: proc(t: ^testing.T) {
e := new_test_editor()
setup_empty_buffer(&e)
buffer := &e.buffers[0]
inputted_text := "Hello, world!"
expected_text := "Hello, beautiful world!\n"
// ------ ---------
// ----------
// 0 1 2
run_text_insertion(&e, inputted_text)
// Move the cursor to the space in between 'Hello,' and 'world!'
run_input_multiple(&e, press_key(.H), 6)
run_text_insertion(&e, " beautiful")
// Move right to the start of the slice of ' world!'
run_input_multiple(&e, press_key(.L), 1)
run_text_insertion(&e, " rich")
run_input_multiple(&e, press_key(.I), 1)
run_input_multiple(&e, press_key(.BACKSPACE), 3)
run_input_multiple(&e, press_key(.ESCAPE), 1)
// Move right, passed the 'h' on to the space before 'world!'
run_input_multiple(&e, press_key(.L), 1)
// Remove the ' h', which consists of two content slices
run_input_multiple(&e, press_key(.I), 1)
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)
contents := buffer_to_string(core.current_buffer(&e))
testing.expectf(t, contents == expected_text, "got '%v', expected '%v'", contents, expected_text)
}
// @(test)
// insert_line_under_current :: proc(t: ^testing.T) {
// e := new_test_editor()
// setup_empty_buffer(&e)
// buffer := &e.buffers[0]
// inputted_text := "Hello, world!\nThis is a new line"
// 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)
// contents := buffer_to_string(core.current_buffer(&e))
// testing.expectf(t, contents == expected_text, "got '%v', expected '%v'", contents, expected_text)
// }
@(test)
move_left_at_beginning_of_file :: proc(t: ^testing.T) {
e := new_test_editor()
setup_empty_buffer(&e)
buffer := &e.buffers[0]
run_text_insertion(&e, "01234")
// Move cursor from --------^
// 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)
// 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)
}
@(test)
move_right_at_end_of_file :: proc(t: ^testing.T) {
e := new_test_editor()
setup_empty_buffer(&e)
is_ctrl_pressed := false
buffer := &e.buffers[0]
run_text_insertion(&e, "01234")
expect_line_col(t, buffer.cursor, 0, 4)
expect_cursor_index(t, buffer.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)
}
@(test)
move_to_end_of_line_from_end :: proc(t: ^testing.T) {
e := new_test_editor()
setup_empty_buffer(&e)
is_ctrl_pressed := false
buffer := &e.buffers[0]
run_text_insertion(&e, "01234\n01234")
// Move up to the first line
run_input_multiple(&e, press_key(.K), 1)
// 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)
}
@(test)
move_to_end_of_line_from_middle :: proc(t: ^testing.T) {
e := new_test_editor()
setup_empty_buffer(&e)
is_ctrl_pressed := false
buffer := &e.buffers[0]
run_text_insertion(&e, "01234\n01234")
// Move up to the first line
run_input_multiple(&e, press_key(.K), 1)
// Move into the middle of the line
run_input_multiple(&e, press_key(.H), 2)
// 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)
}
@(test)
move_to_beginning_of_line_from_middle :: proc(t: ^testing.T) {
e := new_test_editor()
setup_empty_buffer(&e)
is_ctrl_pressed := false
buffer := &e.buffers[0]
run_text_insertion(&e, "01234\n01234")
// Move up to the first line
run_input_multiple(&e, press_key(.K), 1)
// Move into the middle of the line
run_input_multiple(&e, press_key(.H), 2)
// 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)
}
@(test)
move_to_beginning_of_line_from_start :: proc(t: ^testing.T) {
e := new_test_editor()
setup_empty_buffer(&e)
is_ctrl_pressed := false
buffer := &e.buffers[0]
run_text_insertion(&e, "01234\n01234")
// Move up to the first line
run_input_multiple(&e, press_key(.K), 1)
// Move to the start of the line
run_input_multiple(&e, press_key(.H), 4)
// 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)
}
run_editor_frame :: proc(state: ^core.State, input: ArtificialInput, is_ctrl_pressed: ^bool) {
log.infof("running input: %v", input)
{
run_key_action := proc(state: ^core.State, control_key_pressed: bool, key: core.Key) -> bool {
log.info("key_action")
if state.current_input_map != nil {
if control_key_pressed {
if action, exists := state.current_input_map.ctrl_key_actions[key]; exists {
switch value in action.action {
case core.EditorAction:
value(state);
return true;
case core.InputActions:
state.current_input_map = &(&state.current_input_map.ctrl_key_actions[key]).action.(core.InputActions)
return true;
}
}
} else {
if action, exists := state.current_input_map.key_actions[key]; exists {
switch value in action.action {
case core.EditorAction:
value(state);
return true;
case core.InputActions:
state.current_input_map = &(&state.current_input_map.key_actions[key]).action.(core.InputActions)
return true;
}
}
}
} else {
log.info("current_input_map is null")
}
return false
}
switch state.mode {
case .Visual: fallthrough
case .Normal: {
log.info("it's normal/visual mode")
if key, ok := input.(ArtificialKey); ok {
if key.is_down {
if key.key == .LCTRL {
is_ctrl_pressed^ = true;
} else {
run_key_action(state, is_ctrl_pressed^, key.key)
}
} else {
if key.key == .LCTRL {
is_ctrl_pressed^ = false;
}
}
}
}
case .Insert: {
log.info("it's insert mode")
buffer := core.current_buffer(state);
if key, ok := input.(ArtificialKey); ok {
if key.is_down {
// TODO: make this work properly
if true || !run_key_action(state, is_ctrl_pressed^, key.key) {
#partial switch key.key {
case .ESCAPE: {
state.mode = .Normal;
core.insert_content(buffer, buffer.input_buffer[:]);
runtime.clear(&buffer.input_buffer);
}
case .TAB: {
// TODO: change this to insert a tab character
for _ in 0..<4 {
append(&buffer.input_buffer, ' ');
}
}
case .BACKSPACE: {
core.delete_content(buffer, 1);
}
case .ENTER: {
append(&buffer.input_buffer, '\n');
}
}
}
}
}
log.info("before text input")
if text_input, ok := input.(ArtificialTextInput); ok {
log.infof("attempting to append '%v' to buffer", text_input)
for char in text_input.text {
if char < 1 {
break;
}
if char == '\n' || (char >= 32 && char <= 125 && len(buffer.input_buffer) < 1024-1) {
log.infof("appening '%v' to buffer", char)
append(&buffer.input_buffer, u8(char));
}
}
if current_panel, ok := state.current_panel.?; ok {
if panel, ok := util.get(&state.panels, current_panel).?; ok && panel.on_buffer_input_proc != nil {
panel.on_buffer_input_proc(state, &panel.panel_state)
}
}
}
}
}
}
// TODO: share this with the main application
do_insert_mode :: proc(state: ^core.State, buffer: ^core.FileBuffer) {
key := 0;
for key > 0 {
if key >= 32 && key <= 125 && len(buffer.input_buffer) < 1024-1 {
append(&buffer.input_buffer, u8(key));
}
key = 0;
}
}
switch state.mode {
case .Normal:
// buffer := core.current_buffer(state);
// do_normal_mode(state, buffer);
case .Insert:
buffer := core.current_buffer(state);
do_insert_mode(state, buffer);
case .Visual:
// buffer := core.current_buffer(state);
// do_visual_mode(state, buffer);
}
runtime.free_all(context.temp_allocator);
}

18
todo.md
View File

@ -2,22 +2,27 @@
- Fix crash when cursor is over a new-line
- Fix jumping forward a word jumping past consecutive brackets
- Odd scrolling behavior on small screen heights
- Closing the only panel crashes
# Planned Features
- Testing Harness
- [x] Replay user inputs and assert buffer contents/changes
- [ ] Finish writing tests for all current user actions
- Vim-like Macro replays
- [ ] Simple File Search (vim /)
- [ ] Auto-indent
- Modify input system to allow for keybinds that take input
- Vim's f and F movement commands
- Vim's r command
- Save/Load files
- [x] Save
- [ ] Load when changed on disk
- [ ] Simple File Search (vim /)
- [ ] Auto-indent
- Testing Harness
- [ ] Replay user inputs and assert buffer contents/changes
- LSP Integration
- [ ] Language Server Configurations
- [ ] Diagnostics
- [ ] In-line errors
- [ ] Go-to Definition/
- [ ] Find references
- Vim-like Macro replays
- Re-implement lost features from Plugins
- [ ] Syntax Highlighting
- [ ] Integrate tree-sitter
@ -45,9 +50,6 @@
- [ ] Change inside delimiter
- Virtual Whitespace
- Allow any-sized tabs
- Modify input system to allow for keybinds that take input
- Vim's f and F movement commands
- Vim's r command
- Command Search and Execution
- Refactor to remove generics added specifically for plugins
- Palette based UI?