package main import "core:os" import "core:path/filepath" import "core:math" import "core:strings" import "core:runtime" import "core:fmt" import "core:mem" import "core:slice" import "vendor:raylib" import "core" import "theme" import "ui" import "plugin" State :: core.State; FileBuffer :: core.FileBuffer; // TODO: use buffer list in state do_normal_mode :: proc(state: ^State, buffer: ^FileBuffer) { if state.current_input_map != nil { if raylib.IsKeyPressed(.ESCAPE) { core.request_window_close(state); } else if raylib.IsKeyDown(.LEFT_CONTROL) { for key, action in &state.current_input_map.ctrl_key_actions { if raylib.IsKeyPressed(key) { switch value in action.action { case core.EditorAction: value(state); case core.InputMap: state.current_input_map = &(&state.current_input_map.ctrl_key_actions[key]).action.(core.InputMap) } } } } else { for key, action in state.current_input_map.key_actions { if raylib.IsKeyPressed(key) { switch value in action.action { case core.EditorAction: value(state); case core.InputMap: state.current_input_map = &(&state.current_input_map.key_actions[key]).action.(core.InputMap) } } } } } } // TODO: use buffer list in state do_insert_mode :: proc(state: ^State, buffer: ^FileBuffer) { key := raylib.GetCharPressed(); for key > 0 { if key >= 32 && key <= 125 && len(buffer.input_buffer) < 1024-1 { append(&buffer.input_buffer, u8(key)); } key = raylib.GetCharPressed(); } if raylib.IsKeyPressed(.ENTER) { append(&buffer.input_buffer, '\n'); } if raylib.IsKeyPressed(.ESCAPE) { state.mode = .Normal; core.insert_content(buffer, buffer.input_buffer[:]); runtime.clear(&buffer.input_buffer); return; } if raylib.IsKeyPressed(.BACKSPACE) { core.delete_content(buffer, 1); } } switch_to_buffer :: proc(state: ^State, item: ^ui.MenuBarItem) { for buffer, index in state.buffers { if strings.compare(buffer.file_path, item.text) == 0 { state.current_buffer = index; break; } } } register_default_leader_actions :: proc(input_map: ^core.InputMap) { core.register_key_action(input_map, .B, proc(state: ^State) { state.window = ui.create_buffer_list_window(); state.current_input_map = &state.window.input_map; }, "show list of open buffers"); core.register_key_action(input_map, .R, proc(state: ^State) { state.window = ui.create_grep_window(); state.current_input_map = &state.window.input_map; state.mode = .Insert; }, "live grep"); core.register_key_action(input_map, .Q, proc(state: ^State) { state.current_input_map = &state.input_map; }, "close this help"); } register_default_go_actions :: proc(input_map: ^core.InputMap) { core.register_key_action(input_map, .H, proc(state: ^State) { core.move_cursor_start_of_line(&state.buffers[state.current_buffer]); state.current_input_map = &state.input_map; }, "move to beginning of line"); core.register_key_action(input_map, .L, proc(state: ^State) { core.move_cursor_end_of_line(&state.buffers[state.current_buffer]); state.current_input_map = &state.input_map; }, "move to end of line"); } register_default_input_actions :: proc(input_map: ^core.InputMap) { // Cursor Movement { core.register_key_action(input_map, .W, proc(state: ^State) { core.move_cursor_forward_start_of_word(&state.buffers[state.current_buffer]); }, "move forward one word"); core.register_key_action(input_map, .E, proc(state: ^State) { core.move_cursor_forward_end_of_word(&state.buffers[state.current_buffer]); }, "move forward to end of word"); core.register_key_action(input_map, .B, proc(state: ^State) { core.move_cursor_backward_start_of_word(&state.buffers[state.current_buffer]); }, "move backward one word"); core.register_key_action(input_map, .K, proc(state: ^State) { core.move_cursor_up(&state.buffers[state.current_buffer]); }, "move up one line"); core.register_key_action(input_map, .J, proc(state: ^State) { core.move_cursor_down(&state.buffers[state.current_buffer]); }, "move down one line"); core.register_key_action(input_map, .H, proc(state: ^State) { core.move_cursor_left(&state.buffers[state.current_buffer]); }, "move left one char"); core.register_key_action(input_map, .L, proc(state: ^State) { core.move_cursor_right(&state.buffers[state.current_buffer]); }, "move right one char"); core.register_ctrl_key_action(input_map, .U, proc(state: ^State) { core.scroll_file_buffer(&state.buffers[state.current_buffer], .Up); }, "scroll buffer up"); core.register_ctrl_key_action(input_map, .D, proc(state: ^State) { core.scroll_file_buffer(&state.buffers[state.current_buffer], .Down); }, "scroll buffer up"); } // Scale font size { core.register_ctrl_key_action(input_map, .MINUS, proc(state: ^State) { if state.source_font_height > 16 { state.source_font_height -= 2; state.source_font_width = state.source_font_height / 2; state.font = raylib.LoadFontEx("/System/Library/Fonts/Supplemental/Andale Mono.ttf", i32(state.source_font_height*2), nil, 0); raylib.SetTextureFilter(state.font.texture, .BILINEAR); } }, "increase font size"); core.register_ctrl_key_action(input_map, .EQUAL, proc(state: ^State) { state.source_font_height += 2; state.source_font_width = state.source_font_height / 2; state.font = raylib.LoadFontEx("/System/Library/Fonts/Supplemental/Andale Mono.ttf", i32(state.source_font_height*2), nil, 0); raylib.SetTextureFilter(state.font.texture, .BILINEAR); }, "decrease font size"); } // Inserting Text { core.register_key_action(input_map, .I, proc(state: ^State) { state.mode = .Insert; }, "enter insert mode"); core.register_key_action(input_map, .A, proc(state: ^State) { core.move_cursor_right(&state.buffers[state.current_buffer], false); state.mode = .Insert; }, "enter insert mode after character (append)"); } core.register_key_action(input_map, .SPACE, core.new_input_map(), "leader commands"); register_default_leader_actions(&(&input_map.key_actions[.SPACE]).action.(core.InputMap)); core.register_key_action(input_map, .G, core.new_input_map(), "Go commands"); register_default_go_actions(&(&input_map.key_actions[.G]).action.(core.InputMap)); } load_plugins :: proc(state: ^State) -> core.Error { if loaded_plugin, succ := plugin.try_load_plugin("bin/odin_highlighter.dylib"); succ { loaded_plugin.plugin = plugin.Plugin { state = cast(rawptr)state, iter = plugin.Iterator { get_current_buffer_iterator = proc "c" (state: rawptr) -> plugin.BufferIter { state := cast(^State)state; context = state.ctx; it := core.new_file_buffer_iter(&state.buffers[state.current_buffer]); // TODO: make this into a function return plugin.BufferIter { cursor = plugin.Cursor { col = it.cursor.col, line = it.cursor.line, index = plugin.BufferIndex { slice_index = it.cursor.index.slice_index, content_index = it.cursor.index.content_index, } }, buffer = cast(rawptr)it.buffer, hit_end = it.hit_end, } }, get_char_at_iter = proc "c" (state: rawptr, it: ^plugin.BufferIter) -> u8 { state := cast(^State)state; context = state.ctx; internal_it := core.FileBufferIter { cursor = core.Cursor { col = it.cursor.col, line = it.cursor.line, index = core.FileBufferIndex { slice_index = it.cursor.index.slice_index, content_index = it.cursor.index.content_index, } }, buffer = cast(^core.FileBuffer)it.buffer, hit_end = it.hit_end, } return core.get_character_at_iter(internal_it); }, iterate_buffer = proc "c" (state: rawptr, it: ^plugin.BufferIter) -> plugin.IterateResult { state := cast(^State)state; context = state.ctx; // TODO: make this into a function internal_it := core.FileBufferIter { cursor = core.Cursor { col = it.cursor.col, line = it.cursor.line, index = core.FileBufferIndex { slice_index = it.cursor.index.slice_index, content_index = it.cursor.index.content_index, } }, buffer = cast(^core.FileBuffer)it.buffer, hit_end = it.hit_end, } char, _, cond := core.iterate_file_buffer(&internal_it); it^ = plugin.BufferIter { cursor = plugin.Cursor { col = internal_it.cursor.col, line = internal_it.cursor.line, index = plugin.BufferIndex { slice_index = internal_it.cursor.index.slice_index, content_index = internal_it.cursor.index.content_index, } }, buffer = cast(rawptr)internal_it.buffer, hit_end = internal_it.hit_end, }; return plugin.IterateResult { char = char, should_stop = cond, }; }, iterate_buffer_reverse = proc "c" (state: rawptr, it: ^plugin.BufferIter) -> plugin.IterateResult { state := cast(^State)state; context = state.ctx; // TODO: make this into a function internal_it := core.FileBufferIter { cursor = core.Cursor { col = it.cursor.col, line = it.cursor.line, index = core.FileBufferIndex { slice_index = it.cursor.index.slice_index, content_index = it.cursor.index.content_index, } }, buffer = cast(^core.FileBuffer)it.buffer, hit_end = it.hit_end, } char, _, cond := core.iterate_file_buffer_reverse(&internal_it); it^ = plugin.BufferIter { cursor = plugin.Cursor { col = internal_it.cursor.col, line = internal_it.cursor.line, index = plugin.BufferIndex { slice_index = internal_it.cursor.index.slice_index, content_index = internal_it.cursor.index.content_index, } }, buffer = cast(rawptr)internal_it.buffer, hit_end = internal_it.hit_end, }; return plugin.IterateResult { char = char, should_stop = cond, }; }, iterate_buffer_until = proc "c" (state: rawptr, it: ^plugin.BufferIter, until_proc: rawptr) { state := cast(^State)state; context = state.ctx; // TODO: make this into a function internal_it := core.FileBufferIter { cursor = core.Cursor { col = it.cursor.col, line = it.cursor.line, index = core.FileBufferIndex { slice_index = it.cursor.index.slice_index, content_index = it.cursor.index.content_index, } }, buffer = cast(^core.FileBuffer)it.buffer, hit_end = it.hit_end, } core.iterate_file_buffer_until(&internal_it, transmute(core.UntilProc)until_proc); it^ = plugin.BufferIter { cursor = plugin.Cursor { col = internal_it.cursor.col, line = internal_it.cursor.line, index = plugin.BufferIndex { slice_index = internal_it.cursor.index.slice_index, content_index = internal_it.cursor.index.content_index, } }, buffer = cast(rawptr)internal_it.buffer, hit_end = internal_it.hit_end, }; }, until_line_break = transmute(rawptr)core.until_line_break, until_single_quote = transmute(rawptr)core.until_single_quote, until_double_quote = transmute(rawptr)core.until_double_quote, until_end_of_word = transmute(rawptr)core.until_end_of_word, }, buffer = plugin.Buffer { get_buffer_info = proc "c" (state: rawptr) -> plugin.BufferInfo { state := cast(^State)state; context = state.ctx; buffer := &state.buffers[state.current_buffer]; return plugin.BufferInfo { glyph_buffer_width = buffer.glyph_buffer_width, glyph_buffer_height = buffer.glyph_buffer_height, top_line = buffer.top_line, }; }, color_char_at = proc "c" (state: rawptr, start_cursor: plugin.Cursor, end_cursor: plugin.Cursor, palette_index: i32) { state := cast(^State)state; context = state.ctx; buffer := &state.buffers[state.current_buffer]; start_cursor := core.Cursor { col = start_cursor.col, line = start_cursor.line, index = core.FileBufferIndex { slice_index = start_cursor.index.slice_index, content_index = start_cursor.index.content_index, } }; end_cursor := core.Cursor { col = end_cursor.col, line = end_cursor.line, index = core.FileBufferIndex { slice_index = end_cursor.index.slice_index, content_index = end_cursor.index.content_index, } }; core.color_character(buffer, start_cursor, end_cursor, cast(theme.PaletteColor)palette_index); } } }; append(&state.plugins, loaded_plugin); fmt.println("Loaded Odin Highlighter plugin"); return core.no_error(); } return core.make_error(.PluginLoadError, fmt.aprintf("failed to load Odin Highligher plugin")); } main :: proc() { state := State { source_font_width = 8, source_font_height = 16, input_map = core.new_input_map(), window = nil, directory = os.get_current_directory(), plugins = make([dynamic]plugin.Interface), }; state.current_input_map = &state.input_map; register_default_input_actions(&state.input_map); for arg in os.args[1:] { buffer, err := core.new_file_buffer(context.allocator, arg, state.directory); if err.type != .None { fmt.println("Failed to create file buffer:", err); continue; } runtime.append(&state.buffers, buffer); } buffer_items := make([dynamic]ui.MenuBarItem, 0, len(state.buffers)); for buffer, index in state.buffers { item := ui.MenuBarItem { text = buffer.file_path, on_click = switch_to_buffer, }; runtime.append(&buffer_items, item); } // Load plugins if err := load_plugins(&state); err.type != .None { fmt.println(err.msg); } for plugin in state.plugins { if plugin.on_initialize != nil { plugin.on_initialize(plugin.plugin); } } raylib.InitWindow(640, 480, "odin_editor - [back to basics]"); raylib.SetWindowState({ .WINDOW_RESIZABLE, .VSYNC_HINT }); raylib.SetTargetFPS(60); raylib.SetExitKey(.KEY_NULL); // TODO: don't just hard code a MacOS font path state.font = raylib.LoadFontEx("/System/Library/Fonts/Supplemental/Andale Mono.ttf", i32(state.source_font_height*2), nil, 0); raylib.SetTextureFilter(state.font.texture, .BILINEAR); menu_bar_state := ui.MenuBarState{ items = []ui.MenuBarItem { ui.MenuBarItem { text = "Buffers", sub_items = buffer_items[:], } } }; for !raylib.WindowShouldClose() && !state.should_close { state.screen_width = int(raylib.GetScreenWidth()); state.screen_height = int(raylib.GetScreenHeight()); mouse_pos := raylib.GetMousePosition(); buffer := &state.buffers[state.current_buffer]; buffer.glyph_buffer_height = math.min(256, int((state.screen_height - state.source_font_height*2) / state.source_font_height)) + 1; buffer.glyph_buffer_width = math.min(256, int((state.screen_width - state.source_font_width) / state.source_font_width)); { raylib.BeginDrawing(); defer raylib.EndDrawing(); raylib.ClearBackground(theme.get_palette_raylib_color(.Background)); // TODO: be more granular in /what/ is being draw by the plugin for plugin in state.plugins { if plugin.on_initialize != nil { //plugin.on_draw(plugin.plugin); } } core.draw_file_buffer(&state, buffer, 32, state.source_font_height, state.font); ui.draw_menu_bar(&state, &menu_bar_state, 0, 0, i32(state.screen_width), i32(state.screen_height), state.source_font_height); raylib.DrawRectangle(0, i32(state.screen_height - state.source_font_height), i32(state.screen_width), i32(state.source_font_height), theme.get_palette_raylib_color(.Background2)); line_info_text := raylib.TextFormat( // "Line: %d, Col: %d, Len: %d --- Slice Index: %d, Content Index: %d", "Line: %d, Col: %d", buffer.cursor.line + 1, buffer.cursor.col + 1, // core.file_buffer_line_length(buffer, buffer.cursor.index), // buffer.cursor.index.slice_index, // buffer.cursor.index.content_index ); line_info_width := raylib.MeasureTextEx(state.font, line_info_text, f32(state.source_font_height), 0).x; switch state.mode { case .Normal: raylib.DrawRectangle( 0, i32(state.screen_height - state.source_font_height), i32(8 + len("NORMAL")*state.source_font_width), i32(state.source_font_height), theme.get_palette_raylib_color(.Foreground4)); raylib.DrawRectangleV( raylib.Vector2 { f32(state.screen_width) - line_info_width - 8, f32(state.screen_height - state.source_font_height) }, raylib.Vector2 { 8 + line_info_width, f32(state.source_font_height) }, theme.get_palette_raylib_color(.Foreground4)); raylib.DrawTextEx( state.font, "NORMAL", raylib.Vector2 { 4, f32(state.screen_height - state.source_font_height) }, f32(state.source_font_height), 0, theme.get_palette_raylib_color(.Background1)); case .Insert: raylib.DrawRectangle( 0, i32(state.screen_height - state.source_font_height), i32(8 + len("INSERT")*state.source_font_width), i32(state.source_font_height), theme.get_palette_raylib_color(.Foreground2)); raylib.DrawRectangleV( raylib.Vector2 { f32(state.screen_width) - line_info_width - 8, f32(state.screen_height - state.source_font_height) }, raylib.Vector2 { 8 + line_info_width, f32(state.source_font_height) }, theme.get_palette_raylib_color(.Foreground2)); raylib.DrawTextEx( state.font, "INSERT", raylib.Vector2 { 4, f32(state.screen_height - state.source_font_height) }, f32(state.source_font_height), 0, theme.get_palette_raylib_color(.Background1)); } relative_file_path, _ := filepath.rel(state.directory, buffer.file_path) raylib.DrawTextEx( state.font, raylib.TextFormat("%s", relative_file_path), raylib.Vector2 { 8 + 4 + 6 * f32(state.source_font_width), f32(state.screen_height - state.source_font_height) }, f32(state.source_font_height), 0, theme.get_palette_raylib_color(.Foreground1)); raylib.DrawTextEx( state.font, line_info_text, raylib.Vector2 { f32(state.screen_width) - line_info_width - 4, f32(state.screen_height - state.source_font_height) }, f32(state.source_font_height), 0, theme.get_palette_raylib_color(.Background1)); if state.window != nil && state.window.draw != nil { state.window->draw(&state); } if state.current_input_map != &state.input_map { longest_description := 0; for key, action in state.current_input_map.key_actions { if len(action.description) > longest_description { longest_description = len(action.description); } } for key, action in state.current_input_map.ctrl_key_actions { if len(action.description) > longest_description { longest_description = len(action.description); } } longest_description += 8; helper_height := state.source_font_height * (len(state.current_input_map.key_actions) + len(state.current_input_map.ctrl_key_actions)); offset_from_bottom := state.source_font_height * 2; raylib.DrawRectangle( i32(state.screen_width - longest_description * state.source_font_width), i32(state.screen_height - helper_height - offset_from_bottom), i32(longest_description*state.source_font_width), i32(helper_height), theme.get_palette_raylib_color(.Background2)); index := 0; for key, action in state.current_input_map.key_actions { raylib.DrawTextEx( state.font, raylib.TextFormat("%s - %s", key, action.description), raylib.Vector2 { f32(state.screen_width - longest_description * state.source_font_width), f32(state.screen_height - helper_height + index * state.source_font_height - offset_from_bottom) }, f32(state.source_font_height), 0, theme.get_palette_raylib_color(.Foreground1)); index += 1; } for key, action in state.current_input_map.ctrl_key_actions { raylib.DrawTextEx( state.font, raylib.TextFormat("-%s - %s", key, action.description), raylib.Vector2 { f32(state.screen_width - longest_description * state.source_font_width), f32(state.screen_height - helper_height + index * state.source_font_height - offset_from_bottom) }, f32(state.source_font_height), 0, theme.get_palette_raylib_color(.Foreground1)); index += 1; } } } switch state.mode { case .Normal: if state.window != nil && state.window.get_buffer != nil { do_normal_mode(&state, state.window->get_buffer()); } else { do_normal_mode(&state, buffer); } case .Insert: if state.window != nil && state.window.get_buffer != nil { do_insert_mode(&state, state.window->get_buffer()); } else { do_insert_mode(&state, buffer); } } if state.should_close_window { state.should_close_window = false; core.close_window_and_free(&state); } ui.test_menu_bar(&state, &menu_bar_state, 0,0, mouse_pos, raylib.IsMouseButtonReleased(.LEFT), state.source_font_height); } for plugin in state.plugins { if plugin.on_exit != nil { plugin.on_exit(); } } }