diff --git a/plugins/lua/view.lua b/plugins/lua/view.lua index a001a22..4b65973 100644 --- a/plugins/lua/view.lua +++ b/plugins/lua/view.lua @@ -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 - flags = {"DrawText"} + list_with_iter(ctx, "buffer list", BufferSearchIndex, buffer_list_iter(BufferSearchIndex), + function(ctx, buffer_info, is_selected) + 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) + if is_selected then + table.insert(flags, 1, "DrawBorder") end - UI.pop_parent(ctx) - UI.buffer(ctx, BufferSearchIndex) - UI.pop_parent(ctx) + + interaction = UI.advanced_button(ctx, " "..buffer_info.file_path.." ", flags, UI.Fill, UI.FitText) + end + ) + UI.buffer(ctx, BufferSearchIndex) + + -- 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 - flags = {"DrawText"} + list(ctx, "command list", CommandSearchIndex, CommandList, + function(ctx, cmd, is_selected) + flags = {"DrawText"} - if i == CommandSearchIndex then - table.insert(flags, 1, "DrawBorder") - end - interaction = UI.advanced_button(ctx, " "..cmd.name..": "..cmd.description.." ", flags, UI.Fill, UI.FitText) + if is_selected then + table.insert(flags, 1, "DrawBorder") end - UI.pop_parent(ctx) - UI.pop_parent(ctx) + + interaction = UI.advanced_button(ctx, " "..cmd.name..": "..cmd.description.." ", flags, UI.Fill, UI.FitText) + end + ) 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 diff --git a/src/core/core.odin b/src/core/core.odin index 702bf48..170c9e9 100644 --- a/src/core/core.odin +++ b/src/core/core.odin @@ -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 +} diff --git a/src/core/file_buffer.odin b/src/core/file_buffer.odin index 75a7422..17f0562 100644 --- a/src/core/file_buffer.odin +++ b/src/core/file_buffer.odin @@ -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.. (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); diff --git a/todo.md b/todo.md index c8eec23..c42dd34 100644 --- a/todo.md +++ b/todo.md @@ -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)