editor commands + text deletion

memory-refactor
Patrick Cleavelin 2025-02-19 19:03:20 -06:00
parent f0eae25439
commit 402205a2e3
5 changed files with 320 additions and 58 deletions

View File

@ -25,8 +25,8 @@ local MovingTabInBetween = false
local LastMouseX = 0
local LastMouseY = 0
function buffer_list_iter()
local idx = 0
function buffer_list_iter(start)
local idx = start
return function ()
buffer_info = Editor.buffer_info_from_index(idx)
idx = idx + 1
@ -49,6 +49,30 @@ function centered(ctx, label, axis, width, height, body)
UI.pop_parent(ctx)
end
function list_iter(start, list)
local idx = start
return function()
local value = list[idx]
idx = idx + 1
return value, idx-1
end
end
function list(ctx, label, selection_index, list, render_func)
list_with_iter(ctx, label, selection_index, list_iter(selection_index, list), render_func)
end
function list_with_iter(ctx, label, selection_index, list_iter, render_func)
local num_items = 10
UI.push_parent(ctx, UI.push_rect(ctx, label, true, true, UI.Vertical, UI.Fill, UI.Fill))
for data, i in list_iter do
render_func(ctx, data, i == selection_index)
end
UI.pop_parent(ctx)
end
function lerp(from, to, rate)
return (1 - rate) * from + rate*to
end
@ -97,7 +121,7 @@ function ui_sidemenu(ctx)
UI.pop_parent(ctx)
UI.push_rect(ctx, "padded bottom open files", false, false, UI.Horizontal, UI.Fill, UI.Exact(8))
for buffer_info, i in buffer_list_iter() do
for buffer_info, i in buffer_list_iter(0) do
button_container = UI.push_rect(ctx, "button container"..i, false, false, UI.Horizontal, UI.Fill, UI.ChildrenSum)
UI.push_parent(ctx, button_container)
flags = {"Clickable", "Hoverable", "DrawText"}
@ -309,19 +333,32 @@ function render_buffer_search(ctx)
UI.push_parent(ctx, UI.push_floating(ctx, "buffer search canvas", 0, 0))
centered(ctx, "buffer search window", UI.Horizontal, UI.PercentOfParent(window_percent), UI.PercentOfParent(window_percent), (
function ()
UI.push_parent(ctx, UI.push_rect(ctx, "window", true, true, UI.Horizontal, UI.Fill, UI.Fill))
UI.push_parent(ctx, UI.push_rect(ctx, "buffer list", false, false, UI.Vertical, UI.Fill, UI.Fill))
for buffer_info, i in buffer_list_iter() do
list_with_iter(ctx, "buffer list", BufferSearchIndex, buffer_list_iter(BufferSearchIndex),
function(ctx, buffer_info, is_selected)
flags = {"DrawText"}
if i == BufferSearchIndex then
if is_selected then
table.insert(flags, 1, "DrawBorder")
end
interaction = UI.advanced_button(ctx, " "..buffer_info.file_path.." ", flags, UI.Fill, UI.FitText)
end
UI.pop_parent(ctx)
)
UI.buffer(ctx, BufferSearchIndex)
UI.pop_parent(ctx)
-- UI.push_parent(ctx, UI.push_rect(ctx, "window", true, true, UI.Horizontal, UI.Fill, UI.Fill))
-- UI.push_parent(ctx, UI.push_rect(ctx, "buffer list", false, false, UI.Vertical, UI.Fill, UI.Fill))
-- for buffer_info, i in buffer_list_iter() do
-- flags = {"DrawText"}
--
-- if i == BufferSearchIndex then
-- table.insert(flags, 1, "DrawBorder")
-- end
-- interaction = UI.advanced_button(ctx, " "..buffer_info.file_path.." ", flags, UI.Fill, UI.FitText)
-- end
-- UI.pop_parent(ctx)
-- UI.buffer(ctx, BufferSearchIndex)
-- UI.pop_parent(ctx)
end
))
UI.pop_parent(ctx)
@ -346,23 +383,21 @@ function render_command_search(ctx)
end
UI.push_parent(ctx, UI.push_floating(ctx, "buffer search canvas", 0, 0))
centered(ctx, "command search window", UI.Horizontal, UI.PercentOfParent(window_percent_width), UI.PercentOfParent(window_percent_height), (
centered(ctx, "command search window", UI.Horizontal, UI.PercentOfParent(window_percent_width), UI.PercentOfParent(window_percent_height),
function ()
UI.push_parent(ctx, UI.push_rect(ctx, "window", true, true, UI.Horizontal, UI.Fill, UI.Fill))
UI.push_parent(ctx, UI.push_rect(ctx, "command list", false, false, UI.Vertical, UI.Fill, UI.Fill))
-- local commands = Editor.query_command_group("nl.spacegirl.editor.core")
for i, cmd in ipairs(CommandList) do
list(ctx, "command list", CommandSearchIndex, CommandList,
function(ctx, cmd, is_selected)
flags = {"DrawText"}
if i == CommandSearchIndex then
if is_selected then
table.insert(flags, 1, "DrawBorder")
end
interaction = UI.advanced_button(ctx, " "..cmd.name..": "..cmd.description.." ", flags, UI.Fill, UI.FitText)
end
UI.pop_parent(ctx)
UI.pop_parent(ctx)
)
end
))
)
UI.pop_parent(ctx)
end
end
@ -416,7 +451,7 @@ function OnInit()
end
end)},
{Editor.Key.Space, "", {
{Editor.Key.Backtick, "Command Palette",
{Editor.Key.P, "Command Palette",
(function ()
CommandSearchOpen = true
CommandSearchIndex = 1

View File

@ -1,6 +1,7 @@
package core
import "base:runtime"
import "base:intrinsics"
import "core:reflect"
import "core:fmt"
import "core:log"
@ -78,6 +79,8 @@ State :: struct {
current_input_map: ^InputActions,
commands: EditorCommandList,
command_arena: runtime.Allocator,
command_args: [dynamic]EditorCommandArgument,
plugins: [dynamic]plugin.Interface,
plugin_vtable: plugin.Plugin,
@ -92,6 +95,16 @@ EditorCommand :: struct {
action: EditorAction,
}
EditorCommandExec :: struct {
num_args: int,
args: [dynamic]EditorCommandArgument,
}
EditorCommandArgument :: union {
string,
i32
}
current_buffer :: proc(state: ^State) -> ^FileBuffer {
if state.current_buffer == -2 {
return &state.log_buffer;
@ -123,6 +136,7 @@ add_hook :: proc(state: ^State, hook: plugin.Hook, hook_proc: plugin.OnHookProc)
add_lua_hook :: proc(state: ^State, hook: plugin.Hook, hook_ref: LuaHookRef) {
if _, exists := state.lua_hooks[hook]; !exists {
state.lua_hooks[hook] = make([dynamic]LuaHookRef);
log.info("added lua hook", hook)
}
runtime.append(&state.lua_hooks[hook], hook_ref);
@ -289,7 +303,26 @@ query_editor_commands_by_group :: proc(command_list: ^EditorCommandList, name: s
return commands[:];
}
push_command_arg :: proc(state: ^State, command_arg: EditorCommandArgument) {
context.allocator = state.command_arena;
if state.command_args == nil {
state.command_args = make([dynamic]EditorCommandArgument);
}
append(&state.command_args, command_arg)
}
run_command :: proc(state: ^State, group: string, name: string) {
if state.command_args == nil {
state.command_args = make([dynamic]EditorCommandArgument);
}
defer {
state.command_args = nil
runtime.free_all(state.command_arena)
}
if cmds, ok := state.commands[group]; ok {
for cmd in cmds {
if cmd.name == name {
@ -302,3 +335,55 @@ run_command :: proc(state: ^State, group: string, name: string) {
log.error("no command", group, name);
}
attempt_read_command_args :: proc($T: typeid, args: []EditorCommandArgument) -> (value: T, ok: bool)
where intrinsics.type_is_struct(T) {
ti := runtime.type_info_base(type_info_of(T));
#partial switch v in ti.variant {
case runtime.Type_Info_Struct:
{
if int(v.field_count) != len(args) {
ok = false
log.error("invalid number of arguments", len(args), ", expected", v.field_count);
return
}
for arg, i in args {
switch varg in arg {
case string:
{
if _, is_string := v.types[i].variant.(runtime.Type_Info_String); !is_string {
ok = false
log.error("invalid argument #", i, "given to command, found string, expected", v.types[i].variant)
return
}
value_string: ^string = transmute(^string)(uintptr(&value) + v.offsets[i])
value_string^ = varg
}
case i32:
{
if _, is_integer := v.types[i].variant.(runtime.Type_Info_Integer); !is_integer {
ok = false
log.error("invalid argument #", i, "given to command, unexpected integer, expected", v.types[i].variant)
return
}
value_i32: ^i32 = transmute(^i32)(uintptr(&value) + v.offsets[i])
value_i32^ = varg
}
}
}
}
case:
{
return
}
}
ok = true
return
}

View File

@ -1,6 +1,7 @@
package core;
import "core:os"
import "core:log"
import "core:path/filepath"
import "core:mem"
import "core:fmt"
@ -644,6 +645,20 @@ new_selection_span :: proc(start: Cursor, end: Cursor) -> Selection {
new_selection :: proc{new_selection_zero_length, new_selection_span};
swap_selections :: proc(selection: Selection) -> (swapped: Selection) {
swapped = selection
if 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)
{
swapped.start = selection.end
swapped.end = selection.start
}
return swapped
}
new_virtual_file_buffer :: proc(allocator: mem.Allocator) -> FileBuffer {
context.allocator = allocator;
width := 256;
@ -937,16 +952,22 @@ draw_file_buffer :: proc(state: ^State, buffer: ^FileBuffer, x: int, y: int, sho
// NOTE: this requires transparent background color because it renders after the text
// and its after the text because the line length needs to be calculated
if state.mode == .Visual && current_buffer(state) == buffer {
selection := swap_selections(buffer.selection.?)
// selection := buffer.selection.?
sel_x := x + padding;
width: int
if begin+j >= buffer.selection.?.start.line && begin+j <= buffer.selection.?.end.line {
if begin+j == buffer.selection.?.end.line {
width = buffer.selection.?.end.col * state.source_font_width;
if begin+j >= selection.start.line && begin+j <= selection.end.line {
if begin+j == selection.start.line && selection.start.line == selection.end.line {
width = (selection.end.col - selection.start.col) * state.source_font_width;
sel_x += selection.start.col * state.source_font_width;
} else if begin+j == selection.end.line {
width = selection.end.col * state.source_font_width;
} else {
if begin+j == buffer.selection.?.start.line {
width = (line_length - buffer.selection.?.start.col) * state.source_font_width;
sel_x += buffer.selection.?.start.col * state.source_font_width;
if begin+j == selection.start.line {
width = (line_length - selection.start.col) * state.source_font_width;
sel_x += selection.start.col * state.source_font_width;
} else {
width = line_length * state.source_font_width;
}
@ -1027,22 +1048,44 @@ insert_content :: proc(buffer: ^FileBuffer, to_be_inserted: []u8, append_to_end:
}
// TODO: potentially add FileBufferIndex as parameter
split_content_slice :: proc(buffer: ^FileBuffer) {
if buffer.cursor.index.content_index == 0 {
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[buffer.cursor.index.slice_index][buffer.cursor.index.content_index:];
buffer.content_slices[buffer.cursor.index.slice_index] = buffer.content_slices[buffer.cursor.index.slice_index][:buffer.cursor.index.content_index];
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, buffer.cursor.index.slice_index+1, end_slice);
inject_at(&buffer.content_slices, cursor.index.slice_index+1, end_slice);
// TODO: maybe move this out of this function
buffer.cursor.index.slice_index += 1;
buffer.cursor.index.content_index = 0;
cursor.index.slice_index += 1;
cursor.index.content_index = 0;
return true
}
delete_content :: proc(buffer: ^FileBuffer, amount: int) {
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);
} else {
@ -1053,7 +1096,7 @@ delete_content :: proc(buffer: ^FileBuffer, amount: int) {
return;
}
split_content_slice(buffer);
split_content_slice(buffer, &buffer.cursor);
it := new_file_buffer_iter_with_cursor(buffer, buffer.cursor);
@ -1086,3 +1129,27 @@ delete_content :: proc(buffer: ^FileBuffer, amount: int) {
}
}
delete_content_from_selection :: proc(buffer: ^FileBuffer, selection: ^Selection) {
assert(len(buffer.content_slices) >= 1);
selection^ = swap_selections(selection^)
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;
}
delete_content :: proc{delete_content_from_buffer_cursor, delete_content_from_selection};

View File

@ -182,12 +182,13 @@ register_default_visual_actions :: proc(input_map: ^core.InputActions) {
state.current_input_map = &state.input_map.mode[.Normal];
core.current_buffer(state).selection = nil;
core.update_file_buffer_scroll(core.current_buffer(state))
}, "exit visual mode");
// Cursor Movement
{
core.register_key_action(input_map, .W, proc(state: ^State) {
sel_cur := core.current_buffer(state).selection.?;
sel_cur := &(core.current_buffer(state).selection.?);
core.move_cursor_forward_start_of_word(core.current_buffer(state), cursor = &sel_cur.end);
}, "move forward one word");
@ -235,6 +236,20 @@ register_default_visual_actions :: proc(input_map: ^core.InputActions) {
core.scroll_file_buffer(core.current_buffer(state), .Down, cursor = &sel_cur.end);
}, "scroll buffer up");
}
// Text Modification
{
core.register_key_action(input_map, .D, proc(state: ^State) {
sel_cur := &(core.current_buffer(state).selection.?);
core.delete_content(core.current_buffer(state), sel_cur);
state.mode = .Normal
state.current_input_map = &state.input_map.mode[.Normal];
core.current_buffer(state).selection = nil;
core.update_file_buffer_scroll(core.current_buffer(state))
}, "delete selection");
}
}
register_default_text_input_actions :: proc(input_map: ^core.InputActions) {
@ -253,7 +268,6 @@ register_default_text_input_actions :: proc(input_map: ^core.InputActions) {
core.register_key_action(input_map, .O, proc(state: ^State) {
core.move_cursor_end_of_line(core.current_buffer(state), false);
core.insert_content(core.current_buffer(state), []u8{'\n'});
core.move_cursor_down(core.current_buffer(state));
state.mode = .Insert;
sdl2.StartTextInput();
@ -1023,6 +1037,9 @@ lua_ui_flags :: proc(L: ^lua.State, index: i32) -> (bit_set[ui.Flag], bool) {
}
main :: proc() {
_command_arena: mem.Arena
mem.arena_init(&_command_arena, make([]u8, 1024*1024));
state = State {
ctx = context,
screen_width = 640,
@ -1031,6 +1048,7 @@ main :: proc() {
source_font_height = 16,
input_map = core.new_input_map(),
commands = make(core.EditorCommandList),
command_arena = mem.arena_allocator(&_command_arena),
window = nil,
directory = os.get_current_directory(),
@ -1042,9 +1060,28 @@ main :: proc() {
log_buffer = core.new_virtual_file_buffer(context.allocator),
};
// TODO: please move somewhere else
{
ti := runtime.type_info_base(type_info_of(plugin.Hook));
if v, ok := ti.variant.(runtime.Type_Info_Enum); ok {
for i in &v.values {
state.hooks[cast(plugin.Hook)i] = make([dynamic]plugin.OnHookProc);
}
}
}
{
ti := runtime.type_info_base(type_info_of(plugin.Hook));
if v, ok := ti.variant.(runtime.Type_Info_Enum); ok {
for i in &v.values {
state.lua_hooks[cast(plugin.Hook)i] = make([dynamic]core.LuaHookRef);
}
}
}
context.logger = core.new_logger(&state.log_buffer);
state.ctx = context;
// TODO: don't use this
mem.scratch_allocator_init(&scratch, 1024*1024);
scratch_alloc = mem.scratch_allocator(&scratch);
@ -1064,6 +1101,31 @@ main :: proc() {
runtime.append(&state.buffers, buffer);
}
)
core.register_editor_command(
&state.commands,
"nl.spacegirl.editor.core",
"Open File",
"Opens a file in a new buffer",
proc(state: ^State) {
log.info("open file args:");
Args :: struct {
file_path: string
}
if args, ok := core.attempt_read_command_args(Args, state.command_args[:]); ok {
log.info("attempting to open file", args.file_path)
buffer, err := core.new_file_buffer(context.allocator, args.file_path, state.directory);
if err.type != .None {
log.error("Failed to create file buffer:", err);
return;
}
runtime.append(&state.buffers, buffer);
}
}
)
core.register_editor_command(
&state.commands,
"nl.spacegirl.editor.core",
@ -1074,14 +1136,6 @@ main :: proc() {
}
)
{
cmds := core.query_editor_commands_by_group(&state.commands, "nl.spacegirl.editor.core", scratch_alloc);
log.info("List of commands:");
for cmd in cmds {
log.info(cmd.name, ":", cmd.description);
}
}
if len(os.args) > 1 {
for arg in os.args[1:] {
buffer, err := core.new_file_buffer(context.allocator, arg, state.directory);
@ -2156,6 +2210,27 @@ main :: proc() {
sdl2.StopTextInput();
}
case .TAB: {
// TODO: change this to insert a tab character
for _ in 0..<4 {
append(&buffer.input_buffer, ' ');
for hook_proc in state.hooks[plugin.Hook.BufferInput] {
hook_proc(state.plugin_vtable, buffer);
}
for hook_ref in state.lua_hooks[plugin.Hook.BufferInput] {
lua.rawgeti(state.L, lua.REGISTRYINDEX, lua.Integer(hook_ref));
if lua.pcall(state.L, 0, 0, 0) != i32(lua.OK) {
err := lua.tostring(state.L, lua.gettop(state.L));
lua.pop(state.L, lua.gettop(state.L));
log.error(err);
} else {
lua.pop(state.L, lua.gettop(state.L));
}
}
}
}
case .BACKSPACE: {
core.delete_content(buffer, 1);

10
todo.md
View File

@ -1,20 +1,20 @@
- Stack Like Allocator (for cross-frame temp data)
- Undo/Redo
- Edit History Tree
- Finish selections
- Guarantee that start and end are always ordered
- Add in text actions
- Yank
- Delete
- [x] Delete
- Change
- 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
- Palette based UI?
- Registering Plugin Commands that can be run in palette and via other plugins
- A way to query these commands by-plugin
- [ ] Registering Plugin Commands that can be run in palette and via other plugins
- [x] A way to query these commands by-plugin
- Re-write the UI (again)
- Re-do plugin system
- Potentially have a C# plugins system? Use it instead of Lua? (probably not)