package main import "core:c" import "core:fmt" import "core:os" import "base:runtime" import "core:strings" import "core:time" import json "core:encoding/json" import SDL "vendor:sdl3" SDL_View :: struct { first_line: int, tree_first: int, mouse_selecting: bool, tab_dragging: bool, tab_drag_index: int, resizing_panel: Resize_Panel, show_metrics: bool, window_width: int, window_height: int, sidebar_width: int, right_sidebar_width: int, explorer_visible: bool, gradle_sidebar_visible: bool, cached_workspace: string, saved_active_file: int, saved_open_files: [dynamic]Saved_Open_File, } Saved_Open_File :: struct { path: string, cursor: int, } Resize_Panel :: enum { None, Left_Sidebar, Right_Sidebar, } Project_File :: struct { path: string, label: string, depth: int, is_dir: bool, } Project_Tree :: struct { files: [dynamic]Project_File, } Completion_Popup :: struct { open: bool, selected: int, line: int, column: int, pending_id: int, items: [dynamic]Completion_Item, } Completion_Item :: struct { label: string, kind: string, } Hover_Tooltip :: struct { open: bool, pending_id: int, line: int, column: int, contents: string, } Definition_Jump :: struct { pending_id: int, origin_path: string, origin_offset: int, } Nav_Location :: struct { path: string, offset: int, } Navigation_History :: struct { back: [dynamic]Nav_Location, forward: [dynamic]Nav_Location, } Reference_Item :: struct { path: string, line: int, column: int, label: string, } References_Panel :: struct { open: bool, pending_id: int, selected: int, items: [dynamic]Reference_Item, } Rename_Panel :: struct { open: bool, pending_id: int, input: [dynamic]u8, summary: string, edits: [dynamic]string, } Search_Panel :: struct { open: bool, input: [dynamic]u8, message: string, } Diagnostics_Panel :: struct { open: bool, selected: int, } Command_Palette :: struct { open: bool, selected: int, input: [dynamic]u8, } Command_Id :: enum { Reload_Workspace, Save, Save_All, Find, Diagnostics, Gradle_Tasks, Explorer, Metrics, Completion, Hover, Definition, References, Rename, } Command_Item :: struct { id: Command_Id, label: string, } COMMAND_ITEMS := [?]Command_Item{ {.Reload_Workspace, "Reload Workspace"}, {.Save, "Save File"}, {.Save_All, "Save All"}, {.Find, "Find in File"}, {.Diagnostics, "Toggle Diagnostics"}, {.Gradle_Tasks, "Toggle Gradle Tasks"}, {.Explorer, "Toggle Explorer"}, {.Metrics, "Toggle Metrics Overlay"}, {.Completion, "Show Completions"}, {.Hover, "Show Hover"}, {.Definition, "Go to Definition"}, {.References, "Find References"}, {.Rename, "Rename Preview"}, } Gradle_Task_Item :: struct { path: string, name: string, description: string, } Gradle_Tasks_Panel :: struct { open: bool, pending_id: int, pending_run: bool, selected: int, items: [dynamic]Gradle_Task_Item, message: string, output: [dynamic]string, } Syntax_Kind :: enum { Plain, Keyword, String, Comment, Number, Type, } Syntax_Span :: struct { start: int, end: int, kind: Syntax_Kind, } Syntax_State :: struct { in_block_comment: bool, in_triple_string: bool, } Daemon_Sync_State :: struct { pending: bool, last_edit: time.Time, } SDL_WINDOW_WIDTH :: 1100 SDL_WINDOW_HEIGHT :: 760 SDL_TOP_BAR_HEIGHT :: 32 SDL_TAB_BAR_HEIGHT :: 30 SDL_STATUS_BAR_HEIGHT :: 24 SDL_LINE_HEIGHT :: 17 SDL_TREE_ROW_HEIGHT :: 17 SDL_SIDEBAR_DEFAULT_WIDTH :: 260 SDL_SIDEBAR_MIN_WIDTH :: 180 SDL_SIDEBAR_MAX_WIDTH :: 520 SDL_RIGHT_SIDEBAR_DEFAULT_WIDTH :: 340 SDL_RIGHT_SIDEBAR_MIN_WIDTH :: 220 SDL_RIGHT_SIDEBAR_MAX_WIDTH :: 620 SDL_RESIZE_HANDLE_WIDTH :: 6 SDL_DAEMON_SYNC_DEBOUNCE_MS :: 250 SDL_GUTTER_WIDTH :: 72 SDL_EDITOR_TOP :: SDL_TOP_BAR_HEIGHT + SDL_TAB_BAR_HEIGHT SDL_EDITOR_TEXT_Y :: SDL_EDITOR_TOP + 14 SDL_TREE_HEADER_Y :: SDL_TOP_BAR_HEIGHT + 12 SDL_TREE_FIRST_Y :: SDL_TOP_BAR_HEIGHT + 42 ui_state_load :: proc() -> SDL_View { view := SDL_View{ window_width = SDL_WINDOW_WIDTH, window_height = SDL_WINDOW_HEIGHT, sidebar_width = SDL_SIDEBAR_DEFAULT_WIDTH, right_sidebar_width = SDL_RIGHT_SIDEBAR_DEFAULT_WIDTH, explorer_visible = true, } path, ok := ui_state_path(context.temp_allocator) if !ok do return view data, err := os.read_entire_file(path, context.allocator) if err != nil do return view defer delete(data) value, parse_err := json.parse_string(string(data), .JSON, true) if parse_err != nil do return view defer json.destroy_value(value) if width, has_width := json_get_int(value, "sidebarWidth"); has_width { view.sidebar_width = clamp_int(width, SDL_SIDEBAR_MIN_WIDTH, SDL_SIDEBAR_MAX_WIDTH) } if width, has_width := json_get_int(value, "rightSidebarWidth"); has_width { view.right_sidebar_width = clamp_int(width, SDL_RIGHT_SIDEBAR_MIN_WIDTH, SDL_RIGHT_SIDEBAR_MAX_WIDTH) } if width, has_width := json_get_int(value, "windowWidth"); has_width { view.window_width = clamp_int(width, 640, 4096) } if height, has_height := json_get_int(value, "windowHeight"); has_height { view.window_height = clamp_int(height, 420, 4096) } if visible, has_visible := json_get_bool(value, "explorerVisible"); has_visible { view.explorer_visible = visible } if visible, has_visible := json_get_bool(value, "gradleSidebarVisible"); has_visible { view.gradle_sidebar_visible = visible } if workspace, has_workspace := json_get_string(value, "workspace"); has_workspace { view.cached_workspace = strings.clone(workspace) } if active_file, has_active_file := json_get_int(value, "activeFile"); has_active_file { view.saved_active_file = active_file } if open_files_value, has_open_files := json_object_get(value, "openFiles"); has_open_files { #partial switch open_files in open_files_value { case json.Array: for item in open_files { path, has_path := json_get_string(item, "path") if !has_path || len(path) == 0 do continue cursor, _ := json_get_int(item, "cursor") append(&view.saved_open_files, Saved_Open_File{path = strings.clone(path), cursor = max_int(cursor, 0)}) } } } return view } ui_state_save :: proc(view: ^SDL_View) { path, ok := ui_state_path(context.temp_allocator) if !ok do return dir, dir_ok := ui_state_dir(context.temp_allocator) if dir_ok { _ = os.make_directory_all(dir) } open_files_json: [dynamic]u8 defer delete(open_files_json) append(&open_files_json, '[') for file, index in view.saved_open_files { if index > 0 do append(&open_files_json, ',') item := fmt.tprintf("{\"path\":%q,\"cursor\":%d}", file.path, file.cursor) for b in transmute([]u8)item do append(&open_files_json, b) } append(&open_files_json, ']') text := fmt.tprintf("{\n \"windowWidth\": %d,\n \"windowHeight\": %d,\n \"sidebarWidth\": %d,\n \"rightSidebarWidth\": %d,\n \"explorerVisible\": %v,\n \"gradleSidebarVisible\": %v,\n \"workspace\": %q,\n \"activeFile\": %d,\n \"openFiles\": %s\n}\n", view.window_width, view.window_height, view.sidebar_width, view.right_sidebar_width, view.explorer_visible, view.gradle_sidebar_visible, view.cached_workspace, view.saved_active_file, string(open_files_json[:])) _ = os.write_entire_file(path, transmute([]byte)text) } ui_state_destroy :: proc(view: ^SDL_View) { delete(view.cached_workspace) ui_state_clear_open_files(view) delete(view.saved_open_files) } ui_state_clear_open_files :: proc(view: ^SDL_View) { for file in view.saved_open_files { delete(file.path) } clear(&view.saved_open_files) } ui_state_capture_editor :: proc(view: ^SDL_View, editor: ^Editor, workspace: string) { delete(view.cached_workspace) view.cached_workspace = strings.clone(workspace) view.saved_active_file = editor.active ui_state_clear_open_files(view) for &buffer in editor.buffers { append(&view.saved_open_files, Saved_Open_File{path = strings.clone(buffer.path), cursor = buffer.cursor.offset}) } } ui_state_path :: proc(allocator: runtime.Allocator) -> (string, bool) { dir, ok := ui_state_dir(allocator) if !ok do return "", false return fmt.aprintf("%s/ui.json", dir), true } ui_state_dir :: proc(allocator: runtime.Allocator) -> (string, bool) { cache_dir, err := os.user_cache_dir(allocator) if err != nil do return "", false return fmt.aprintf("%s/native-kotlin-editor", cache_dir), true } left_sidebar_width :: proc(view: ^SDL_View) -> int { if !view.explorer_visible do return 0 return clamp_int(view.sidebar_width, SDL_SIDEBAR_MIN_WIDTH, SDL_SIDEBAR_MAX_WIDTH) } right_sidebar_width_view :: proc(view: ^SDL_View, window_width: int) -> int { max_available := max_int(window_width - left_sidebar_width(view) - 220, SDL_RIGHT_SIDEBAR_MIN_WIDTH) return min_int(clamp_int(view.right_sidebar_width, SDL_RIGHT_SIDEBAR_MIN_WIDTH, SDL_RIGHT_SIDEBAR_MAX_WIDTH), max_available) } editor_text_x :: proc(view: ^SDL_View) -> int { return left_sidebar_width(view) + SDL_GUTTER_WIDTH + 20 } daemon_sync_schedule :: proc(sync: ^Daemon_Sync_State) { sync.pending = true sync.last_edit = time.now() } daemon_sync_now :: proc(sync: ^Daemon_Sync_State, daemon: ^Daemon_Client, editor: ^Editor, open: bool) { daemon_sync_active_buffer(daemon, editor, open) sync.pending = false } daemon_sync_flush_if_due :: proc(sync: ^Daemon_Sync_State, daemon: ^Daemon_Client, editor: ^Editor) { if !sync.pending || !daemon.connected do return if time.diff(sync.last_edit, time.now()) < SDL_DAEMON_SYNC_DEBOUNCE_MS * time.Millisecond do return daemon_sync_now(sync, daemon, editor, false) } daemon_begin_owned_start :: proc(workspace: string, owned_daemon: ^Daemon_Process, port_line: ^[dynamic]u8, editor: ^Editor, start_pending: ^bool) -> bool { daemon_stop_process(owned_daemon) clear(port_line) started_daemon, ok := daemon_start_process_begin(workspace) if ok { owned_daemon^ = started_daemon start_pending^ = true editor_set_status(editor, "Starting Kotlin daemon...") return true } editor_set_status(editor, "Kotlin daemon failed to start") return false } editor_restore_initial_files :: proc(editor: ^Editor, view: ^SDL_View, workspace, file_path: string) -> bool { if len(file_path) > 0 { return editor_open_file(editor, file_path) } restored := false if view.cached_workspace == workspace && len(view.saved_open_files) > 0 { for file in view.saved_open_files { if !os.is_file(file.path) do continue if editor_open_file(editor, file.path) { active := editor_active_buffer(editor) if active != nil { active.cursor.offset = clamp_int(file.cursor, 0, buffer_len(&active.buffer)) _, active.cursor.wanted_column = buffer_offset_to_line_col(&active.buffer, active.cursor.offset) } restored = true } } if restored { editor.active = clamp_int(view.saved_active_file, 0, len(editor.buffers) - 1) return true } } path := fmt.tprintf("%s/src/main/kotlin/dev/nativeeditor/daemon/Main.kt", workspace) return editor_open_file(editor, path) } run_sdl_editor :: proc(workspace: string, daemon_port: int, file_path: string) { view := ui_state_load() defer ui_state_destroy(&view) editor := Editor{} defer editor_destroy(&editor) if !editor_restore_initial_files(&editor, &view, workspace, file_path) { return } tree := project_tree_load(workspace) defer project_tree_destroy(&tree) if !SDL.Init(SDL.INIT_VIDEO) { fmt.println("SDL init failed:", SDL.GetError()) return } defer SDL.Quit() window := SDL.CreateWindow("Native Kotlin Editor", i32(view.window_width), i32(view.window_height), {.RESIZABLE}) if window == nil { fmt.println("SDL window failed:", SDL.GetError()) return } defer SDL.DestroyWindow(window) gpu := gpu_renderer_make(window) defer gpu_renderer_destroy(&gpu) renderer: ^SDL.Renderer if !gpu.available { renderer = SDL.CreateRenderer(window, nil) if renderer == nil { fmt.println("SDL renderer failed:", SDL.GetError()) return } } defer { if renderer != nil { SDL.DestroyRenderer(renderer) } } _ = SDL.StartTextInput(window) defer { _ = SDL.StopTextInput(window) } default_cursor := SDL.GetDefaultCursor() text_cursor := SDL.CreateSystemCursor(.TEXT) resize_cursor := SDL.CreateSystemCursor(.EW_RESIZE) defer { if text_cursor != nil do SDL.DestroyCursor(text_cursor) if resize_cursor != nil do SDL.DestroyCursor(resize_cursor) } refresh_view_size(window, &view) defer { ui_state_capture_editor(&view, &editor, workspace) ui_state_save(&view) } completion := completion_popup_make() defer completion_popup_destroy(&completion) hover := Hover_Tooltip{} defer hover_tooltip_destroy(&hover) definition := Definition_Jump{} defer definition_jump_destroy(&definition) navigation := Navigation_History{} defer navigation_history_destroy(&navigation) references := References_Panel{} defer references_panel_destroy(&references) rename := Rename_Panel{} defer rename_panel_destroy(&rename) search := Search_Panel{} defer search_panel_destroy(&search) diagnostics_panel := Diagnostics_Panel{} command_palette := Command_Palette{} defer command_palette_destroy(&command_palette) tasks_panel := Gradle_Tasks_Panel{} tasks_panel.open = view.gradle_sidebar_visible if tasks_panel.open { tasks_panel.message = strings.clone("Waiting for Kotlin daemon...") } defer gradle_tasks_panel_destroy(&tasks_panel) owned_daemon := Daemon_Process{} defer daemon_stop_process(&owned_daemon) daemon := Daemon_Client{} defer daemon_close(&daemon) daemon_sync_state := Daemon_Sync_State{} daemon_port_line: [dynamic]u8 defer delete(daemon_port_line) daemon_start_pending := false daemon_connect_pending := false daemon_initialized := false owned_daemon_enabled := daemon_port <= 0 daemon_restart_cooldown_frames := 0 port := daemon_port if port > 0 { daemon_connect_pending = true } else { _ = daemon_begin_owned_start(workspace, &owned_daemon, &daemon_port_line, &editor, &daemon_start_pending) } running := true for running { event: SDL.Event for SDL.PollEvent(&event) { #partial switch event.type { case .QUIT, .WINDOW_CLOSE_REQUESTED: running = false case .KEY_DOWN: if handle_sdl_key(&editor, &view, &tree, workspace, &completion, &hover, &definition, &navigation, &references, &rename, &search, &diagnostics_panel, &command_palette, &tasks_panel, &daemon, &daemon_sync_state, event.key.key, event.key.mod) { daemon_sync_schedule(&daemon_sync_state) } case .TEXT_INPUT: text := strings.truncate_to_byte(string(event.text.text), 0) if len(text) > 0 { if rename.open { rename_panel_insert(&rename, text) continue } if search.open { search_panel_insert(&search, text) continue } if command_palette.open { command_palette_insert(&command_palette, text) continue } active := editor_active_buffer(&editor) if active != nil { editor_insert_text(active, text) close_stale_edit_overlays(&completion, &hover, &references, &rename) ensure_cursor_visible(&editor, &view) daemon_sync_schedule(&daemon_sync_state) } } case .MOUSE_WHEEL: if handle_overlay_wheel(&editor, &view, &gpu, &command_palette, &completion, &references, &diagnostics_panel, event.wheel.mouse_x, event.wheel.mouse_y, int(event.wheel.integer_y)) { // Routed to the topmost hovered overlay. } else if handle_gradle_tasks_wheel(&view, &tasks_panel, event.wheel.mouse_x, event.wheel.mouse_y, int(event.wheel.integer_y)) { // Routed to the right sidebar. } else if event.wheel.mouse_x < f32(left_sidebar_width(&view)) { scroll_project_tree(&tree, &view, -int(event.wheel.integer_y) * 3) } else { scroll_sdl_view(&editor, &view, -int(event.wheel.integer_y) * 3) } case .MOUSE_BUTTON_DOWN: if event.button.button != SDL.BUTTON_LEFT do continue if handle_panel_resize_down(&view, &tasks_panel, event.button.x, event.button.y) { view.mouse_selecting = false set_editor_cursor(&view, &tasks_panel, event.button.x, event.button.y, default_cursor, text_cursor, resize_cursor) } else if handle_command_palette_click(&command_palette, &editor, &view, &tree, workspace, &completion, &hover, &definition, &references, &rename, &search, &diagnostics_panel, &tasks_panel, &daemon, &daemon_sync_state, event.button.x, event.button.y) { view.mouse_selecting = false } else if handle_completion_click(&editor, &view, &gpu, &completion, &hover, &references, &rename, &search, &daemon_sync_state, event.button.x, event.button.y) { view.mouse_selecting = false } else if handle_references_click(&references, &navigation, &editor, &view, event.button.x, event.button.y) { view.mouse_selecting = false } else if handle_diagnostics_click(&diagnostics_panel, &editor, &view, event.button.x, event.button.y) { view.mouse_selecting = false } else if handle_gradle_tasks_click(&view, &tasks_panel, event.button.x, event.button.y) { view.mouse_selecting = false } else if handle_overlay_outside_click(&command_palette, &completion, &references, &diagnostics_panel) { view.mouse_selecting = false } else if handle_editor_tab_click(&editor, &view, event.button.x, event.button.y) { daemon_sync_now(&daemon_sync_state, &daemon, &editor, true) } else if handle_project_tree_click(&editor, &tree, &view, event.button.x, event.button.y) { daemon_sync_now(&daemon_sync_state, &daemon, &editor, true) } else { view.mouse_selecting = handle_editor_text_mouse(&editor, &view, &gpu, event.button.x, event.button.y, false) } case .MOUSE_BUTTON_UP: if event.button.button == SDL.BUTTON_LEFT { if view.resizing_panel != .None { view.resizing_panel = .None ui_state_save(&view) } view.tab_dragging = false view.tab_drag_index = 0 view.mouse_selecting = false } case .MOUSE_MOTION: if view.resizing_panel != .None && (event.motion.state & SDL.BUTTON_LMASK) != {} { handle_panel_resize_motion(&view, event.motion.x) } else if view.tab_dragging && (event.motion.state & SDL.BUTTON_LMASK) != {} { handle_editor_tab_drag(&editor, &view, event.motion.x, event.motion.y) } else if view.mouse_selecting && (event.motion.state & SDL.BUTTON_LMASK) != {} { handle_editor_text_mouse(&editor, &view, &gpu, event.motion.x, event.motion.y, true) } set_editor_cursor(&view, &tasks_panel, event.motion.x, event.motion.y, default_cursor, text_cursor, resize_cursor) } } refresh_view_size(window, &view) if owned_daemon_enabled && !daemon.connected && !daemon_start_pending && !daemon_connect_pending { if daemon_initialized { daemon_initialized = false daemon_close(&daemon) editor_set_status(&editor, "Kotlin daemon disconnected; restarting...") daemon_restart_cooldown_frames = 30 } if daemon_restart_cooldown_frames > 0 { daemon_restart_cooldown_frames -= 1 } else { _ = daemon_begin_owned_start(workspace, &owned_daemon, &daemon_port_line, &editor, &daemon_start_pending) daemon_restart_cooldown_frames = 30 } } if !daemon.connected { if daemon_start_pending { started_port, ready, failed := daemon_poll_port(&owned_daemon, &daemon_port_line) if ready { port = started_port daemon_start_pending = false daemon_connect_pending = true } else if failed { daemon_start_pending = false daemon_stop_process(&owned_daemon) daemon_restart_cooldown_frames = 60 editor_set_status(&editor, "Kotlin daemon failed to announce a port") } } if daemon_connect_pending { daemon = daemon_connect(port) daemon_connect_pending = false if daemon.connected { daemon_start_reader(&daemon) daemon_send_workspace_open(&daemon, workspace) daemon_sync_now(&daemon_sync_state, &daemon, &editor, true) if tasks_panel.open && tasks_panel.pending_id == 0 && len(tasks_panel.items) == 0 { gradle_tasks_panel_request(&tasks_panel, &daemon) } daemon_initialized = true editor_set_status(&editor, "Kotlin daemon ready") } else { if owned_daemon_enabled { daemon_stop_process(&owned_daemon) daemon_restart_cooldown_frames = 60 } editor_set_status(&editor, "Kotlin daemon connection failed") } } } else if !daemon_initialized { daemon_start_reader(&daemon) daemon_send_workspace_open(&daemon, workspace) daemon_sync_now(&daemon_sync_state, &daemon, &editor, true) if tasks_panel.open && tasks_panel.pending_id == 0 && len(tasks_panel.items) == 0 { gradle_tasks_panel_request(&tasks_panel, &daemon) } daemon_initialized = true } daemon_sync_flush_if_due(&daemon_sync_state, &daemon, &editor) daemon_apply_latest_diagnostics(&daemon, &editor) completion_popup_apply_response(&completion, &daemon) hover_tooltip_apply_response(&hover, &daemon) definition_jump_apply_response(&definition, &navigation, &editor, &view, &daemon) references_panel_apply_response(&references, &daemon) rename_panel_apply_response(&rename, &daemon) gradle_tasks_panel_apply_event(&tasks_panel, &daemon) gradle_tasks_panel_apply_response(&tasks_panel, &daemon) if gpu.available { render_sdl_editor_gpu(&gpu, &editor, &view, &tree, &completion, &hover, &references, &rename, &search, &diagnostics_panel, &command_palette, &tasks_panel) } else { render_sdl_editor(renderer, &editor, &view, &tree, &completion, &hover, &references, &rename) } SDL.Delay(16) } } completion_popup_make :: proc() -> Completion_Popup { popup := Completion_Popup{} completion_popup_set_message(&popup, "No completions yet") return popup } completion_popup_destroy :: proc(popup: ^Completion_Popup) { for item in popup.items { delete(item.label) delete(item.kind) } delete(popup.items) } close_stale_edit_overlays :: proc(completion: ^Completion_Popup, hover: ^Hover_Tooltip, references: ^References_Panel, rename: ^Rename_Panel) { completion.open = false hover.open = false references.open = false rename.open = false } close_completion_accept_overlays :: proc(completion: ^Completion_Popup, hover: ^Hover_Tooltip, references: ^References_Panel, rename: ^Rename_Panel, search: ^Search_Panel) { close_stale_edit_overlays(completion, hover, references, rename) search.open = false } completion_popup_open :: proc(popup: ^Completion_Popup, editor: ^Editor, daemon: ^Daemon_Client) { active := editor_active_buffer(editor) if active == nil do return line, column := buffer_offset_to_line_col(&active.buffer, active.cursor.offset) popup.open = true popup.selected = 0 popup.line = line popup.column = column popup.pending_id = daemon_send_completion(daemon, active.path, line, column) completion_popup_set_message(popup, "Loading completions...") } hover_tooltip_destroy :: proc(hover: ^Hover_Tooltip) { delete(hover.contents) } hover_tooltip_open :: proc(hover: ^Hover_Tooltip, editor: ^Editor, daemon: ^Daemon_Client) { active := editor_active_buffer(editor) if active == nil do return line, column := buffer_offset_to_line_col(&active.buffer, active.cursor.offset) hover.open = true hover.line = line hover.column = column hover.pending_id = daemon_send_hover(daemon, active.path, line, column) hover_tooltip_set_contents(hover, "Loading hover...") } definition_jump_request :: proc(definition: ^Definition_Jump, editor: ^Editor, daemon: ^Daemon_Client) { active := editor_active_buffer(editor) if active == nil do return delete(definition.origin_path) definition.origin_path = strings.clone(active.path) definition.origin_offset = active.cursor.offset line, column := buffer_offset_to_line_col(&active.buffer, active.cursor.offset) definition.pending_id = daemon_send_definition(daemon, active.path, line, column) } definition_jump_destroy :: proc(definition: ^Definition_Jump) { delete(definition.origin_path) } definition_jump_apply_response :: proc(definition: ^Definition_Jump, navigation: ^Navigation_History, editor: ^Editor, view: ^SDL_View, daemon: ^Daemon_Client) { if definition.pending_id == 0 do return response := daemon_take_response(daemon, definition.pending_id) defer delete(response) if len(response) == 0 do return path, line, column, ok := parse_definition_response(string(response[:]), definition.pending_id) defer delete(path) if !ok do return definition.pending_id = 0 if len(path) == 0 do return if len(definition.origin_path) > 0 { navigation_push(&navigation.back, definition.origin_path, definition.origin_offset) navigation_clear_stack(&navigation.forward) } if !editor_jump_current_tab_to_location(editor, view, path, line, column) do return daemon_sync_active_buffer(daemon, editor, true) } editor_jump_to_location :: proc(editor: ^Editor, view: ^SDL_View, path: string, line, column: int) -> bool { if !editor_open_or_focus_file(editor, path) do return false active := editor_active_buffer(editor) if active == nil do return false cursor_move_to_line_col(&active.buffer, &active.cursor, max_int(line - 1, 0), max_int(column - 1, 0)) ensure_cursor_visible(editor, view) return true } editor_jump_current_tab_to_location :: proc(editor: ^Editor, view: ^SDL_View, path: string, line, column: int) -> bool { if !editor_replace_active_file(editor, path) do return false active := editor_active_buffer(editor) if active == nil do return false cursor_move_to_line_col(&active.buffer, &active.cursor, max_int(line - 1, 0), max_int(column - 1, 0)) ensure_cursor_visible(editor, view) return true } parse_definition_response :: proc(response: string, expected_id: int) -> (string, int, int, bool) { message, value, ok := parse_protocol_message(response) if !ok do return "", 0, 0, false defer json.destroy_value(value) if message.kind != .Response || message.id != expected_id || !message.ok do return "", 0, 0, false result, has_result := json_object_get(value, "result") if !has_result do return "", 0, 0, false locations_value, has_locations := json_object_get(result, "locations") if !has_locations do return "", 0, 0, false #partial switch locations in locations_value { case json.Array: if len(locations) == 0 do return "", 0, 0, true location := locations[0] path, has_path := json_get_string(location, "path") if !has_path do return "", 0, 0, false line, _ := json_get_int(location, "line") column, _ := json_get_int(location, "column") return strings.clone(path), line, column, true } return "", 0, 0, false } navigation_history_destroy :: proc(navigation: ^Navigation_History) { navigation_clear_stack(&navigation.back) navigation_clear_stack(&navigation.forward) delete(navigation.back) delete(navigation.forward) } navigation_clear_stack :: proc(stack: ^[dynamic]Nav_Location) { for location in stack^ { delete(location.path) } clear(stack) } navigation_push :: proc(stack: ^[dynamic]Nav_Location, path: string, offset: int) { append(stack, Nav_Location{path = strings.clone(path), offset = offset}) } navigation_pop :: proc(stack: ^[dynamic]Nav_Location) -> (Nav_Location, bool) { if len(stack^) == 0 do return {}, false index := len(stack^) - 1 location := stack^[index] resize(stack, index) return location, true } navigation_current_push :: proc(stack: ^[dynamic]Nav_Location, editor: ^Editor) { active := editor_active_buffer(editor) if active == nil do return navigation_push(stack, active.path, active.cursor.offset) } navigation_go_back :: proc(navigation: ^Navigation_History, editor: ^Editor, view: ^SDL_View) { location, ok := navigation_pop(&navigation.back) if !ok do return defer delete(location.path) navigation_current_push(&navigation.forward, editor) if !editor_open_or_focus_file(editor, location.path) do return active := editor_active_buffer(editor) if active == nil do return active.cursor.offset = clamp_int(location.offset, 0, buffer_len(&active.buffer)) _, active.cursor.wanted_column = buffer_offset_to_line_col(&active.buffer, active.cursor.offset) ensure_cursor_visible(editor, view) } navigation_go_forward :: proc(navigation: ^Navigation_History, editor: ^Editor, view: ^SDL_View) { location, ok := navigation_pop(&navigation.forward) if !ok do return defer delete(location.path) navigation_current_push(&navigation.back, editor) if !editor_open_or_focus_file(editor, location.path) do return active := editor_active_buffer(editor) if active == nil do return active.cursor.offset = clamp_int(location.offset, 0, buffer_len(&active.buffer)) _, active.cursor.wanted_column = buffer_offset_to_line_col(&active.buffer, active.cursor.offset) ensure_cursor_visible(editor, view) } references_panel_destroy :: proc(panel: ^References_Panel) { references_panel_clear(panel) } references_panel_clear :: proc(panel: ^References_Panel) { for item in panel.items { delete(item.path) delete(item.label) } clear(&panel.items) } references_panel_request :: proc(panel: ^References_Panel, editor: ^Editor, daemon: ^Daemon_Client) { active := editor_active_buffer(editor) if active == nil do return line, column := buffer_offset_to_line_col(&active.buffer, active.cursor.offset) panel.open = true panel.selected = 0 panel.pending_id = daemon_send_references(daemon, active.path, line, column) references_panel_clear(panel) append(&panel.items, Reference_Item{label = strings.clone("Loading references...")}) } references_panel_apply_response :: proc(panel: ^References_Panel, daemon: ^Daemon_Client) { if !panel.open || panel.pending_id == 0 do return response := daemon_take_response(daemon, panel.pending_id) defer delete(response) if len(response) == 0 do return items, ok := parse_references_response(string(response[:]), panel.pending_id) defer { for item in items { delete(item.path) delete(item.label) } delete(items) } if !ok do return panel.pending_id = 0 references_panel_clear(panel) if len(items) == 0 { append(&panel.items, Reference_Item{label = strings.clone("No references")}) } else { for item in items { append(&panel.items, Reference_Item{ path = strings.clone(item.path), line = item.line, column = item.column, label = strings.clone(item.label), }) } } panel.selected = 0 } parse_references_response :: proc(response: string, expected_id: int) -> ([dynamic]Reference_Item, bool) { items: [dynamic]Reference_Item message, value, ok := parse_protocol_message(response) if !ok do return items, false defer json.destroy_value(value) if message.kind != .Response || message.id != expected_id || !message.ok do return items, false result, has_result := json_object_get(value, "result") if !has_result do return items, false locations_value, has_locations := json_object_get(result, "locations") if !has_locations do return items, false #partial switch locations in locations_value { case json.Array: for location in locations { path, has_path := json_get_string(location, "path") if !has_path do continue line, _ := json_get_int(location, "line") column, _ := json_get_int(location, "column") _, file := os.split_path(path) label := fmt.tprintf("%s:%d:%d", file, line, column) append(&items, Reference_Item{ path = strings.clone(path), line = line, column = column, label = strings.clone(label), }) } } return items, true } references_panel_accept :: proc(panel: ^References_Panel, navigation: ^Navigation_History, editor: ^Editor, view: ^SDL_View) { if !panel.open || len(panel.items) == 0 do return index := clamp_int(panel.selected, 0, len(panel.items) - 1) item := panel.items[index] if len(item.path) == 0 do return navigation_current_push(&navigation.back, editor) navigation_clear_stack(&navigation.forward) _ = editor_jump_to_location(editor, view, item.path, item.line, item.column) panel.open = false } rename_panel_destroy :: proc(panel: ^Rename_Panel) { delete(panel.input) delete(panel.summary) for edit in panel.edits { delete(edit) } delete(panel.edits) } search_panel_destroy :: proc(panel: ^Search_Panel) { delete(panel.input) delete(panel.message) } command_palette_destroy :: proc(palette: ^Command_Palette) { delete(palette.input) } command_palette_open :: proc(palette: ^Command_Palette) { palette.open = true palette.selected = 0 clear(&palette.input) } command_palette_insert :: proc(palette: ^Command_Palette, text: string) { for b in transmute([]u8)text { if b >= 32 && b < 127 { append(&palette.input, b) } } palette.selected = min_int(palette.selected, max_int(command_palette_match_count(palette) - 1, 0)) } command_palette_backspace :: proc(palette: ^Command_Palette) { if len(palette.input) > 0 { resize(&palette.input, len(palette.input) - 1) palette.selected = min_int(palette.selected, max_int(command_palette_match_count(palette) - 1, 0)) } } command_palette_match_count :: proc(palette: ^Command_Palette) -> int { count := 0 for item in COMMAND_ITEMS { if command_palette_matches(palette, item.label) do count += 1 } return count } command_palette_selected_item :: proc(palette: ^Command_Palette) -> (Command_Item, bool) { seen := 0 for item in COMMAND_ITEMS { if !command_palette_matches(palette, item.label) do continue if seen == palette.selected do return item, true seen += 1 } return {}, false } command_palette_matches :: proc(palette: ^Command_Palette, label: string) -> bool { if len(palette.input) == 0 do return true return ascii_contains_fold(label, string(palette.input[:])) } ascii_contains_fold :: proc(haystack, needle: string) -> bool { if len(needle) == 0 do return true if len(needle) > len(haystack) do return false for start := 0; start <= len(haystack) - len(needle); start += 1 { match := true for i := 0; i < len(needle); i += 1 { if ascii_lower(haystack[start + i]) != ascii_lower(needle[i]) { match = false break } } if match do return true } return false } ascii_lower :: proc(b: u8) -> u8 { if b >= 'A' && b <= 'Z' do return b + ('a' - 'A') return b } command_palette_accept :: proc(palette: ^Command_Palette, editor: ^Editor, view: ^SDL_View, tree: ^Project_Tree, workspace: string, completion: ^Completion_Popup, hover: ^Hover_Tooltip, definition: ^Definition_Jump, references: ^References_Panel, rename: ^Rename_Panel, search: ^Search_Panel, diagnostics_panel: ^Diagnostics_Panel, tasks_panel: ^Gradle_Tasks_Panel, daemon: ^Daemon_Client, sync: ^Daemon_Sync_State) { item, ok := command_palette_selected_item(palette) if !ok do return palette.open = false switch item.id { case .Reload_Workspace: workspace_reload(editor, view, tree, workspace, daemon, sync) case .Save: _ = editor_save_active(editor) daemon_sync_now(sync, daemon, editor, false) case .Save_All: _ = editor_save_all(editor) daemon_sync_now(sync, daemon, editor, false) case .Find: search_panel_open(search) case .Diagnostics: diagnostics_panel.open = !diagnostics_panel.open diagnostics_panel.selected = 0 case .Gradle_Tasks: if tasks_panel.open { tasks_panel.open = false view.gradle_sidebar_visible = false } else { view.gradle_sidebar_visible = true daemon_sync_now(sync, daemon, editor, false) gradle_tasks_panel_request(tasks_panel, daemon) } ui_state_save(view) case .Explorer: view.explorer_visible = !view.explorer_visible view.tree_first = 0 ui_state_save(view) case .Metrics: view.show_metrics = !view.show_metrics case .Completion: daemon_sync_now(sync, daemon, editor, false) completion_popup_open(completion, editor, daemon) case .Hover: daemon_sync_now(sync, daemon, editor, false) hover_tooltip_open(hover, editor, daemon) case .Definition: daemon_sync_now(sync, daemon, editor, false) definition_jump_request(definition, editor, daemon) case .References: daemon_sync_now(sync, daemon, editor, false) references_panel_request(references, editor, daemon) case .Rename: rename_panel_open(rename) } } gradle_tasks_panel_destroy :: proc(panel: ^Gradle_Tasks_Panel) { gradle_tasks_panel_clear(panel) delete(panel.items) delete(panel.message) gradle_tasks_panel_clear_output(panel) delete(panel.output) } gradle_tasks_panel_clear :: proc(panel: ^Gradle_Tasks_Panel) { for item in panel.items { delete(item.path) delete(item.name) delete(item.description) } clear(&panel.items) } gradle_tasks_panel_clear_output :: proc(panel: ^Gradle_Tasks_Panel) { for line in panel.output { delete(line) } clear(&panel.output) } gradle_tasks_panel_add_output :: proc(panel: ^Gradle_Tasks_Panel, stream, text: string) { label := fmt.tprintf("%s: %s", stream, text) append(&panel.output, strings.clone(label)) for len(panel.output) > 80 { delete(panel.output[0]) ordered_remove(&panel.output, 0) } } gradle_tasks_panel_request :: proc(panel: ^Gradle_Tasks_Panel, daemon: ^Daemon_Client) { panel.open = true panel.selected = 0 panel.pending_id = daemon_send_gradle_tasks(daemon) panel.pending_run = false gradle_tasks_panel_clear(panel) delete(panel.message) panel.message = strings.clone("Loading Gradle tasks...") } gradle_tasks_panel_apply_response :: proc(panel: ^Gradle_Tasks_Panel, daemon: ^Daemon_Client) { if !panel.open || panel.pending_id == 0 do return response := daemon_take_response(daemon, panel.pending_id) defer delete(response) if len(response) == 0 do return if panel.pending_run { message, ok := parse_gradle_run_response(string(response[:]), panel.pending_id) defer delete(message) if !ok do return panel.pending_id = 0 panel.pending_run = false delete(panel.message) panel.message = strings.clone(message) return } items, ok := parse_gradle_tasks_response(string(response[:]), panel.pending_id) defer { for item in items { delete(item.path) delete(item.name) delete(item.description) } delete(items) } if !ok do return panel.pending_id = 0 gradle_tasks_panel_clear(panel) for item in items { append(&panel.items, Gradle_Task_Item{path = strings.clone(item.path), name = strings.clone(item.name), description = strings.clone(item.description)}) } delete(panel.message) if len(panel.items) == 0 { panel.message = strings.clone("No Gradle tasks") } else { panel.message = strings.clone("Enter will run tasks in a later slice") } } gradle_tasks_panel_apply_event :: proc(panel: ^Gradle_Tasks_Panel, daemon: ^Daemon_Client) { if !panel.open do return for { event := daemon_take_gradle_event(daemon) if len(event) == 0 { delete(event) return } task, state, stream, text, ok := parse_gradle_run_event(string(event[:])) delete(event) if ok { if state == "output" { gradle_tasks_panel_add_output(panel, stream, text) } else { delete(panel.message) panel.message = strings.clone(fmt.tprintf("Gradle %s: %s", state, task)) } } delete(task) delete(state) delete(stream) delete(text) } } parse_gradle_run_event :: proc(event: string) -> (string, string, string, string, bool) { message, value, ok := parse_protocol_message(event) if !ok do return "", "", "", "", false defer json.destroy_value(value) if message.kind != .Event || message.event != "gradle/run" do return "", "", "", "", false params, has_params := json_object_get(value, "params") if !has_params do return "", "", "", "", false task, _ := json_get_string(params, "task") state, _ := json_get_string(params, "state") stream, _ := json_get_string(params, "stream") text, _ := json_get_string(params, "text") return strings.clone(task), strings.clone(state), strings.clone(stream), strings.clone(text), true } gradle_tasks_panel_run_selected :: proc(panel: ^Gradle_Tasks_Panel, daemon: ^Daemon_Client) { if len(panel.items) == 0 || panel.pending_id != 0 do return index := clamp_int(panel.selected, 0, len(panel.items) - 1) task := panel.items[index].path if len(task) == 0 do task = panel.items[index].name if len(task) == 0 do return panel.pending_id = daemon_send_gradle_run(daemon, task) panel.pending_run = true gradle_tasks_panel_clear_output(panel) delete(panel.message) panel.message = strings.clone(fmt.tprintf("Running %s...", task)) } parse_gradle_run_response :: proc(response: string, expected_id: int) -> (string, bool) { message, value, ok := parse_protocol_message(response) if !ok do return "", false defer json.destroy_value(value) if message.kind != .Response || message.id != expected_id { return "", false } if !message.ok { code, _ := json_get_string(value, "code") error_message, _ := json_get_string(value, "message") if len(error_message) > 0 { return strings.clone(fmt.tprintf("Gradle failed: %s", error_message)), true } return strings.clone(fmt.tprintf("Gradle failed: %s", code)), true } result, has_result := json_object_get(value, "result") task := "task" if has_result { task, _ = json_get_string(result, "task") } return strings.clone(fmt.tprintf("Gradle finished: %s", task)), true } parse_gradle_tasks_response :: proc(response: string, expected_id: int) -> ([dynamic]Gradle_Task_Item, bool) { items: [dynamic]Gradle_Task_Item message, value, ok := parse_protocol_message(response) if !ok do return items, false defer json.destroy_value(value) if message.kind != .Response || message.id != expected_id || !message.ok do return items, false result, has_result := json_object_get(value, "result") if !has_result do return items, false tasks_value, has_tasks := json_object_get(result, "tasks") if !has_tasks do return items, false #partial switch tasks in tasks_value { case json.Array: for task in tasks { path, _ := json_get_string(task, "path") name, _ := json_get_string(task, "name") description, _ := json_get_string(task, "description") append(&items, Gradle_Task_Item{path = strings.clone(path), name = strings.clone(name), description = strings.clone(description)}) } } return items, true } search_panel_open :: proc(panel: ^Search_Panel) { panel.open = true delete(panel.message) panel.message = strings.clone("Enter search text, then Enter") } search_panel_insert :: proc(panel: ^Search_Panel, text: string) { for b in transmute([]u8)text { if b >= 32 && b < 127 { append(&panel.input, b) } } } search_panel_backspace :: proc(panel: ^Search_Panel) { if len(panel.input) > 0 { resize(&panel.input, len(panel.input) - 1) } } search_panel_find_next :: proc(panel: ^Search_Panel, editor: ^Editor, view: ^SDL_View) { search_panel_find(panel, editor, view, 1) } search_panel_find_previous :: proc(panel: ^Search_Panel, editor: ^Editor, view: ^SDL_View) { search_panel_find(panel, editor, view, -1) } search_panel_find :: proc(panel: ^Search_Panel, editor: ^Editor, view: ^SDL_View, direction: int) { active := editor_active_buffer(editor) if active == nil do return if len(panel.input) == 0 { search_panel_set_message(panel, "Search text is empty") return } text := buffer_bytes(&active.buffer) defer delete(text) query := panel.input[:] found: int ok: bool if direction >= 0 { start := min_int(active.cursor.offset + 1, len(text)) found, ok = find_bytes_from(text[:], query, start) if !ok && start > 0 { found, ok = find_bytes_from(text[:], query, 0) } } else { start := max_int(active.cursor.offset - len(query) - 1, 0) found, ok = find_bytes_reverse_from(text[:], query, start) if !ok && start < len(text) { found, ok = find_bytes_reverse_from(text[:], query, len(text) - len(query)) } } if !ok { search_panel_set_message(panel, "No matches") return } active.selection_active = true active.selection_anchor = found active.cursor.offset = found + len(query) _, active.cursor.wanted_column = buffer_offset_to_line_col(&active.buffer, active.cursor.offset) ensure_cursor_visible(editor, view) line, column := buffer_offset_to_line_col(&active.buffer, found) search_panel_set_message(panel, fmt.tprintf("Found at %d:%d", line + 1, column + 1)) } search_panel_set_message :: proc(panel: ^Search_Panel, message: string) { delete(panel.message) panel.message = strings.clone(message) } find_bytes_from :: proc(text: []u8, query: []u8, start: int) -> (int, bool) { if len(query) == 0 || len(query) > len(text) do return 0, false i := clamp_int(start, 0, len(text) - len(query)) for i <= len(text) - len(query) { match := true for j := 0; j < len(query); j += 1 { if text[i + j] != query[j] { match = false break } } if match do return i, true i += 1 } return 0, false } find_bytes_reverse_from :: proc(text: []u8, query: []u8, start: int) -> (int, bool) { if len(query) == 0 || len(query) > len(text) do return 0, false i := clamp_int(start, 0, len(text) - len(query)) for i >= 0 { match := true for j := 0; j < len(query); j += 1 { if text[i + j] != query[j] { match = false break } } if match do return i, true i -= 1 } return 0, false } rename_panel_open :: proc(panel: ^Rename_Panel) { panel.open = true panel.pending_id = 0 clear(&panel.input) delete(panel.summary) panel.summary = strings.clone("Enter new name, then Enter for preview") for edit in panel.edits { delete(edit) } clear(&panel.edits) } rename_panel_insert :: proc(panel: ^Rename_Panel, text: string) { for b in transmute([]u8)text { if (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9') || b == '_' { append(&panel.input, b) } } } rename_panel_backspace :: proc(panel: ^Rename_Panel) { if len(panel.input) > 0 { resize(&panel.input, len(panel.input) - 1) } } rename_panel_request :: proc(panel: ^Rename_Panel, editor: ^Editor, daemon: ^Daemon_Client) { active := editor_active_buffer(editor) if active == nil do return if len(panel.input) == 0 { delete(panel.summary) panel.summary = strings.clone("New name is empty") return } line, column := buffer_offset_to_line_col(&active.buffer, active.cursor.offset) panel.pending_id = daemon_send_rename(daemon, active.path, line, column, string(panel.input[:])) delete(panel.summary) panel.summary = strings.clone("Loading rename preview...") } rename_panel_apply_response :: proc(panel: ^Rename_Panel, daemon: ^Daemon_Client) { if !panel.open || panel.pending_id == 0 do return response := daemon_take_response(daemon, panel.pending_id) defer delete(response) if len(response) == 0 do return summary, edits, ok := parse_rename_response(string(response[:]), panel.pending_id) defer { delete(summary) for edit in edits { delete(edit) } delete(edits) } if !ok do return panel.pending_id = 0 delete(panel.summary) panel.summary = strings.clone(summary) for edit in panel.edits { delete(edit) } clear(&panel.edits) for edit in edits { append(&panel.edits, strings.clone(edit)) } } parse_rename_response :: proc(response: string, expected_id: int) -> (string, [dynamic]string, bool) { edits: [dynamic]string message, value, ok := parse_protocol_message(response) if !ok do return "", edits, false defer json.destroy_value(value) if message.kind != .Response || message.id != expected_id || !message.ok do return "", edits, false result, has_result := json_object_get(value, "result") if !has_result do return "", edits, false old_name, _ := json_get_string(result, "oldName") new_name, _ := json_get_string(result, "newName") edits_value, has_edits := json_object_get(result, "edits") if !has_edits do return "", edits, false #partial switch items in edits_value { case json.Array: for item in items { path, has_path := json_get_string(item, "path") if !has_path do continue line, _ := json_get_int(item, "line") column, _ := json_get_int(item, "column") _, file := os.split_path(path) label := fmt.tprintf("%s:%d:%d", file, line, column) append(&edits, strings.clone(label)) } } summary := fmt.tprintf("Preview: %s -> %s (%d edits)", old_name, new_name, len(edits)) return strings.clone(summary), edits, true } hover_tooltip_apply_response :: proc(hover: ^Hover_Tooltip, daemon: ^Daemon_Client) { if !hover.open || hover.pending_id == 0 do return response := daemon_take_response(daemon, hover.pending_id) defer delete(response) if len(response) == 0 do return contents, ok := parse_hover_response(string(response[:]), hover.pending_id) defer delete(contents) if !ok do return hover.pending_id = 0 if len(contents) == 0 { hover_tooltip_set_contents(hover, "No hover information") } else { hover_tooltip_set_contents(hover, contents) } } hover_tooltip_set_contents :: proc(hover: ^Hover_Tooltip, contents: string) { delete(hover.contents) hover.contents = strings.clone(contents) } parse_hover_response :: proc(response: string, expected_id: int) -> (string, bool) { message, value, ok := parse_protocol_message(response) if !ok do return "", false defer json.destroy_value(value) if message.kind != .Response || message.id != expected_id || !message.ok do return "", false result, has_result := json_object_get(value, "result") if !has_result do return "", false contents, has_contents := json_get_string(result, "contents") if !has_contents do return "", true return strings.clone(contents), true } completion_popup_apply_response :: proc(popup: ^Completion_Popup, daemon: ^Daemon_Client) { if !popup.open || popup.pending_id == 0 do return response := daemon_take_response(daemon, popup.pending_id) defer delete(response) if len(response) == 0 do return items, ok := parse_completion_response(string(response[:]), popup.pending_id) defer { for item in items { delete(item.label) delete(item.kind) } delete(items) } if !ok do return popup.pending_id = 0 if len(items) == 0 { completion_popup_set_message(popup, "No completions") } else { completion_popup_set_items(popup, items[:]) } } completion_popup_set_message :: proc(popup: ^Completion_Popup, message: string) { completion_popup_set_items(popup, []Completion_Item{{label = message, kind = "status"}}) } completion_popup_set_items :: proc(popup: ^Completion_Popup, items: []Completion_Item) { for item in popup.items { delete(item.label) delete(item.kind) } clear(&popup.items) for item in items { append(&popup.items, Completion_Item{label = strings.clone(item.label), kind = strings.clone(item.kind)}) } popup.selected = 0 } completion_popup_accept :: proc(popup: ^Completion_Popup, editor: ^Editor) -> bool { if !popup.open || len(popup.items) == 0 do return false active := editor_active_buffer(editor) if active == nil do return false index := clamp_int(popup.selected, 0, len(popup.items) - 1) item := popup.items[index] if item.kind == "status" || len(item.label) == 0 do return false selection_start, selection_end, has_selection := editor_selection_range(active) if has_selection { buffer_replace_range(&active.buffer, selection_start, selection_end - selection_start, item.label) active.cursor.offset = selection_start + len(item.label) _, active.cursor.wanted_column = buffer_offset_to_line_col(&active.buffer, active.cursor.offset) editor_update_dirty(active) editor_clear_selection(active) popup.open = false return true } start := active.cursor.offset for start > 0 { bytes := buffer_range_bytes(&active.buffer, start - 1, 1) if len(bytes) == 0 { delete(bytes) break } ch := bytes[0] delete(bytes) if !is_identifier_part_byte(ch) do break start -= 1 } if start < active.cursor.offset { buffer_replace_range(&active.buffer, start, active.cursor.offset - start, item.label) active.cursor.offset = start active.cursor.offset += len(item.label) } else { buffer_replace_range(&active.buffer, active.cursor.offset, 0, item.label) active.cursor.offset += len(item.label) } _, active.cursor.wanted_column = buffer_offset_to_line_col(&active.buffer, active.cursor.offset) editor_update_dirty(active) editor_clear_selection(active) popup.open = false return true } parse_completion_response :: proc(response: string, expected_id: int) -> ([dynamic]Completion_Item, bool) { labels: [dynamic]Completion_Item message, value, ok := parse_protocol_message(response) if !ok do return labels, false defer json.destroy_value(value) if message.kind != .Response || message.id != expected_id || !message.ok do return labels, false result, has_result := json_object_get(value, "result") if !has_result do return labels, false items_value, has_items := json_object_get(result, "items") if !has_items do return labels, false #partial switch items in items_value { case json.Array: for item in items { label := "" kind := "" #partial switch value in item { case json.String: label = string(value) case json.Object: label, _ = json_get_string(item, "label") kind, _ = json_get_string(item, "kind") } if len(label) > 0 { append(&labels, Completion_Item{label = strings.clone(label), kind = strings.clone(kind)}) } } } return labels, true } project_tree_load :: proc(workspace: string) -> Project_Tree { tree := Project_Tree{} project_tree_append_dir(&tree, workspace, "", 0) return tree } project_tree_destroy :: proc(tree: ^Project_Tree) { for file in tree.files { delete(file.path) delete(file.label) } delete(tree.files) tree.files = nil } workspace_reload :: proc(editor: ^Editor, view: ^SDL_View, tree: ^Project_Tree, workspace: string, daemon: ^Daemon_Client, sync: ^Daemon_Sync_State) { project_tree_destroy(tree) tree^ = project_tree_load(workspace) view.tree_first = 0 editor_clear_all_diagnostics(editor) daemon_send_workspace_open(daemon, workspace) daemon_sync_now(sync, daemon, editor, true) editor_set_status(editor, "Workspace reloaded") } project_tree_append_dir :: proc(tree: ^Project_Tree, dir: string, prefix: string, depth: int) { if depth > 5 || len(tree.files) >= 300 do return entries, err := os.read_all_directory_by_path(dir, context.allocator) if err != nil do return defer os.file_info_slice_delete(entries, context.allocator) for entry in entries { if len(tree.files) >= 300 do return if project_tree_skip(entry.name) do continue path := fmt.tprintf("%s/%s", dir, entry.name) label := fmt.tprintf("%s%s", prefix, entry.name) append(&tree.files, Project_File{ path = strings.clone(path), label = strings.clone(label), depth = depth, is_dir = entry.type == .Directory, }) if entry.type == .Directory { child_prefix := fmt.tprintf("%s ", prefix) project_tree_append_dir(tree, path, child_prefix, depth + 1) } } } project_tree_skip :: proc(name: string) -> bool { return name == ".git" || name == ".gradle" || name == "build" || name == ".idea" } set_editor_cursor :: proc(view: ^SDL_View, tasks_panel: ^Gradle_Tasks_Panel, x, y: f32, default_cursor, text_cursor, resize_cursor: ^SDL.Cursor) { if view.resizing_panel != .None || panel_resize_hit(view, tasks_panel, x, y) { if resize_cursor != nil do _ = SDL.SetCursor(resize_cursor) return } if editor_text_hit(view, tasks_panel, x, y) { if text_cursor != nil do _ = SDL.SetCursor(text_cursor) return } if default_cursor != nil do _ = SDL.SetCursor(default_cursor) } panel_resize_hit :: proc(view: ^SDL_View, tasks_panel: ^Gradle_Tasks_Panel, x, y: f32) -> bool { if view.explorer_visible { left_edge := f32(left_sidebar_width(view)) if abs_f32(x - left_edge) <= SDL_RESIZE_HANDLE_WIDTH && y >= SDL_TOP_BAR_HEIGHT { return true } } if tasks_panel != nil && tasks_panel.open { right_edge := f32(view.window_width - right_sidebar_width_view(view, view.window_width)) if abs_f32(x - right_edge) <= SDL_RESIZE_HANDLE_WIDTH && y >= SDL_TOP_BAR_HEIGHT { return true } } return false } handle_panel_resize_down :: proc(view: ^SDL_View, tasks_panel: ^Gradle_Tasks_Panel, x, y: f32) -> bool { if view.explorer_visible { left_edge := f32(left_sidebar_width(view)) if abs_f32(x - left_edge) <= SDL_RESIZE_HANDLE_WIDTH && y >= SDL_TOP_BAR_HEIGHT { view.resizing_panel = .Left_Sidebar return true } } if tasks_panel != nil && tasks_panel.open { right_edge := f32(view.window_width - right_sidebar_width_view(view, view.window_width)) if abs_f32(x - right_edge) <= SDL_RESIZE_HANDLE_WIDTH && y >= SDL_TOP_BAR_HEIGHT { view.resizing_panel = .Right_Sidebar return true } } return false } editor_text_hit :: proc(view: ^SDL_View, tasks_panel: ^Gradle_Tasks_Panel, x, y: f32) -> bool { if y < SDL_EDITOR_TEXT_Y || y >= f32(view.window_height - SDL_STATUS_BAR_HEIGHT) do return false if x < f32(editor_text_x(view)) do return false right_limit := f32(view.window_width - 8) if tasks_panel != nil && tasks_panel.open { right_limit = f32(view.window_width - right_sidebar_width_view(view, view.window_width) - 8) } return x < right_limit } handle_panel_resize_motion :: proc(view: ^SDL_View, x: f32) { #partial switch view.resizing_panel { case .Left_Sidebar: max_width := min_int(SDL_SIDEBAR_MAX_WIDTH, max_int(view.window_width - view.right_sidebar_width - 260, SDL_SIDEBAR_MIN_WIDTH)) view.sidebar_width = clamp_int(int(x), SDL_SIDEBAR_MIN_WIDTH, max_width) case .Right_Sidebar: available := view.window_width - left_sidebar_width(view) - 220 max_width := min_int(SDL_RIGHT_SIDEBAR_MAX_WIDTH, max_int(available, SDL_RIGHT_SIDEBAR_MIN_WIDTH)) view.right_sidebar_width = clamp_int(view.window_width - int(x), SDL_RIGHT_SIDEBAR_MIN_WIDTH, max_width) } } handle_project_tree_click :: proc(editor: ^Editor, tree: ^Project_Tree, view: ^SDL_View, x, y: f32) -> bool { if x >= f32(left_sidebar_width(view)) do return false if y < SDL_TREE_FIRST_Y do return false index := view.tree_first + int((y - SDL_TREE_FIRST_Y) / SDL_TREE_ROW_HEIGHT) if index < 0 || index >= len(tree.files) do return false item := tree.files[index] if item.is_dir do return false if !editor_open_or_focus_file(editor, item.path) do return false view.first_line = 0 return true } handle_editor_tab_click :: proc(editor: ^Editor, view: ^SDL_View, x, y: f32) -> bool { index, tab_x, width, ok := editor_tab_hit(editor, view, x, y) if !ok do return false if x >= tab_x + width - 28 { _ = editor_close_buffer(editor, index) view.tab_dragging = false return true } editor.active = index view.tab_dragging = true view.tab_drag_index = index return true } handle_editor_tab_drag :: proc(editor: ^Editor, view: ^SDL_View, x, y: f32) { if !view.tab_dragging || len(editor.buffers) <= 1 do return target, _, _, ok := editor_tab_hit(editor, view, x, y) if !ok || target == view.tab_drag_index do return editor_move_buffer(editor, view.tab_drag_index, target) view.tab_drag_index = target } handle_command_palette_click :: proc(palette: ^Command_Palette, editor: ^Editor, view: ^SDL_View, tree: ^Project_Tree, workspace: string, completion: ^Completion_Popup, hover: ^Hover_Tooltip, definition: ^Definition_Jump, references: ^References_Panel, rename: ^Rename_Panel, search: ^Search_Panel, diagnostics_panel: ^Diagnostics_Panel, tasks_panel: ^Gradle_Tasks_Panel, daemon: ^Daemon_Client, sync: ^Daemon_Sync_State, x, y: f32) -> bool { if !palette.open do return false width := f32(min_int(max_int(view.window_width - 280, 420), 720)) palette_x := f32(view.window_width) * 0.5 - width * 0.5 palette_y: f32 = SDL_TOP_BAR_HEIGHT + 48 match_count := command_palette_match_count(palette) visible := min_int(max_int(match_count, 1), 8) height := f32(58 + visible * 24) if x < palette_x || x >= palette_x + width || y < palette_y || y >= palette_y + height do return false if y < palette_y + 68 do return true if match_count == 0 do return true first_seen := 0 if palette.selected >= 8 { first_seen = palette.selected - 7 } row := int((y - (palette_y + 68)) / 24) target_seen := first_seen + row if target_seen >= 0 && target_seen < match_count { palette.selected = target_seen command_palette_accept(palette, editor, view, tree, workspace, completion, hover, definition, references, rename, search, diagnostics_panel, tasks_panel, daemon, sync) } return true } handle_completion_click :: proc(editor: ^Editor, view: ^SDL_View, gpu: ^GPU_Renderer, popup: ^Completion_Popup, hover: ^Hover_Tooltip, references: ^References_Panel, rename: ^Rename_Panel, search: ^Search_Panel, sync: ^Daemon_Sync_State, x, y: f32) -> bool { if !popup.open do return false active := editor_active_buffer(editor) if active == nil do return false cursor_line, cursor_col := buffer_offset_to_line_col(&active.buffer, active.cursor.offset) popup_x := editor_column_pixel_x_gpu(gpu, view, &active.buffer, cursor_line, cursor_col) popup_y := f32(SDL_EDITOR_TEXT_Y + (cursor_line - view.first_line + 1) * SDL_LINE_HEIGHT) if popup_y > f32(view.window_height - 110) do popup_y = f32(view.window_height - 110) visible_items := min_int(len(popup.items), 8) if visible_items <= 0 do return x >= popup_x && x < popup_x + 260 && y >= popup_y && y < popup_y + 26 height := f32(26 + visible_items * 16) if x < popup_x || x >= popup_x + 260 || y < popup_y || y >= popup_y + height do return false if y < popup_y + 22 do return true first_index := 0 if popup.selected >= 8 { first_index = popup.selected - 7 } row := int((y - (popup_y + 22)) / 16) index := first_index + row if index >= 0 && index < len(popup.items) { popup.selected = index if completion_popup_accept(popup, editor) { close_completion_accept_overlays(popup, hover, references, rename, search) ensure_cursor_visible(editor, view) daemon_sync_schedule(sync) } } return true } handle_references_click :: proc(panel: ^References_Panel, navigation: ^Navigation_History, editor: ^Editor, view: ^SDL_View, x, y: f32) -> bool { if !panel.open do return false popup_x: f32 = 700 popup_y: f32 = 72 visible := min_int(max_int(len(panel.items), 1), 14) height := f32(30 + visible * 16) if x < popup_x || x >= popup_x + 380 || y < popup_y || y >= popup_y + height do return false if y < popup_y + 28 do return true first_index := 0 if panel.selected >= 14 { first_index = panel.selected - 13 } row := int((y - (popup_y + 28)) / 16) index := first_index + row if index >= 0 && index < len(panel.items) { panel.selected = index references_panel_accept(panel, navigation, editor, view) } return true } handle_diagnostics_click :: proc(panel: ^Diagnostics_Panel, editor: ^Editor, view: ^SDL_View, x, y: f32) -> bool { if !panel.open do return false active := editor_active_buffer(editor) if active == nil do return false height: f32 = 168 sidebar_width := left_sidebar_width(view) panel_x := f32(sidebar_width) panel_y := f32(view.window_height - SDL_STATUS_BAR_HEIGHT) - height width := f32(view.window_width - sidebar_width) if x < panel_x || x >= panel_x + width || y < panel_y || y >= panel_y + height do return false if y < panel_y + 38 do return true if len(active.diagnostics) == 0 do return true panel.selected = clamp_int(panel.selected, 0, len(active.diagnostics) - 1) visible_items := 7 first_index := 0 if panel.selected >= visible_items { first_index = panel.selected - visible_items + 1 } row := int((y - (panel_y + 38)) / 18) index := first_index + row if index >= 0 && index < len(active.diagnostics) && index < first_index + visible_items { panel.selected = index diagnostics_panel_accept(panel, editor, view) } return true } handle_gradle_tasks_click :: proc(view: ^SDL_View, panel: ^Gradle_Tasks_Panel, x, y: f32) -> bool { if !panel.open do return false width := f32(right_sidebar_width_view(view, view.window_width)) height := f32(view.window_height - SDL_TOP_BAR_HEIGHT - SDL_STATUS_BAR_HEIGHT) panel_x := f32(view.window_width) - width panel_y: f32 = SDL_TOP_BAR_HEIGHT if x < panel_x || x >= panel_x + width || y < panel_y || y >= panel_y + height do return false output_height: f32 = 0 if len(panel.output) > 0 { output_height = min_f32(160, max_f32(72, height * 0.32)) } visible_items := max_int(int((height - output_height - 104) / 18), 1) if len(panel.items) == 0 || y < panel_y + 92 do return true panel.selected = clamp_int(panel.selected, 0, len(panel.items) - 1) first_index := 0 if panel.selected >= visible_items { first_index = panel.selected - visible_items + 1 } row := int((y - (panel_y + 92)) / 18) index := first_index + row if index >= 0 && index < len(panel.items) && index < first_index + visible_items { panel.selected = index } return true } handle_gradle_tasks_wheel :: proc(view: ^SDL_View, panel: ^Gradle_Tasks_Panel, x, y: f32, wheel_y: int) -> bool { if !panel.open do return false width := f32(right_sidebar_width_view(view, view.window_width)) panel_x := f32(view.window_width) - width panel_y: f32 = SDL_TOP_BAR_HEIGHT panel_height := f32(view.window_height - SDL_TOP_BAR_HEIGHT - SDL_STATUS_BAR_HEIGHT) if x < panel_x || x >= panel_x + width || y < panel_y || y >= panel_y + panel_height do return false if len(panel.items) == 0 do return true panel.selected = clamp_int(panel.selected - wheel_y * 3, 0, len(panel.items) - 1) return true } handle_overlay_outside_click :: proc(command_palette: ^Command_Palette, completion: ^Completion_Popup, references: ^References_Panel, diagnostics_panel: ^Diagnostics_Panel) -> bool { if command_palette.open { command_palette.open = false return true } if completion.open { completion.open = false return true } if references.open { references.open = false return true } if diagnostics_panel.open { diagnostics_panel.open = false return true } return false } handle_overlay_wheel :: proc(editor: ^Editor, view: ^SDL_View, gpu: ^GPU_Renderer, command_palette: ^Command_Palette, completion: ^Completion_Popup, references: ^References_Panel, diagnostics_panel: ^Diagnostics_Panel, x, y: f32, wheel_y: int) -> bool { if command_palette.open { width := f32(min_int(max_int(view.window_width - 280, 420), 720)) palette_x := f32(view.window_width) * 0.5 - width * 0.5 palette_y: f32 = SDL_TOP_BAR_HEIGHT + 48 visible := min_int(max_int(command_palette_match_count(command_palette), 1), 8) height := f32(58 + visible * 24) if x >= palette_x && x < palette_x + width && y >= palette_y && y < palette_y + height { max_selected := max_int(command_palette_match_count(command_palette) - 1, 0) command_palette.selected = clamp_int(command_palette.selected - wheel_y * 3, 0, max_selected) return true } } if completion.open { active := editor_active_buffer(editor) if active != nil { cursor_line, cursor_col := buffer_offset_to_line_col(&active.buffer, active.cursor.offset) popup_x := editor_column_pixel_x_gpu(gpu, view, &active.buffer, cursor_line, cursor_col) popup_y := f32(SDL_EDITOR_TEXT_Y + (cursor_line - view.first_line + 1) * SDL_LINE_HEIGHT) if popup_y > f32(view.window_height - 110) do popup_y = f32(view.window_height - 110) visible_items := min_int(len(completion.items), 8) height := f32(26 + visible_items * 16) if x >= popup_x && x < popup_x + 260 && y >= popup_y && y < popup_y + height { completion.selected = clamp_int(completion.selected - wheel_y * 3, 0, max_int(len(completion.items) - 1, 0)) return true } } } if references.open { popup_x: f32 = 700 popup_y: f32 = 72 visible := min_int(max_int(len(references.items), 1), 14) height := f32(30 + visible * 16) if x >= popup_x && x < popup_x + 380 && y >= popup_y && y < popup_y + height { references.selected = clamp_int(references.selected - wheel_y * 3, 0, max_int(len(references.items) - 1, 0)) return true } } if diagnostics_panel.open { active := editor_active_buffer(editor) if active != nil { height: f32 = 168 sidebar_width := left_sidebar_width(view) panel_x := f32(sidebar_width) panel_y := f32(view.window_height - SDL_STATUS_BAR_HEIGHT) - height width := f32(view.window_width - sidebar_width) if x >= panel_x && x < panel_x + width && y >= panel_y && y < panel_y + height { diagnostics_panel.selected = clamp_int(diagnostics_panel.selected - wheel_y * 3, 0, max_int(len(active.diagnostics) - 1, 0)) return true } } } return false } editor_tab_hit :: proc(editor: ^Editor, view: ^SDL_View, x, y: f32) -> (int, f32, f32, bool) { sidebar_width := left_sidebar_width(view) if x < f32(sidebar_width) || y < SDL_TOP_BAR_HEIGHT || y >= SDL_EDITOR_TOP do return 0, 0, 0, false tab_x := f32(sidebar_width + 12) max_x := f32(view.window_width - 12) for &buffer, index in editor.buffers { if tab_x >= max_x do break _, file_name := os.split_path(buffer.path) width := f32(tab_width_for_label(file_name)) if tab_x + width > max_x { width = max_x - tab_x } if x >= tab_x && x < tab_x + width { return index, tab_x, width, true } tab_x += width + 4 } return 0, 0, 0, false } editor_move_buffer :: proc(editor: ^Editor, from, to: int) -> bool { if from < 0 || from >= len(editor.buffers) || to < 0 || to >= len(editor.buffers) || from == to do return false buffer := editor.buffers[from] ordered_remove(&editor.buffers, from) inject_at(&editor.buffers, to, buffer) editor.active = to return true } tab_width_for_label :: proc(label: string) -> int { return max_int(150, min_int(280, gpu_text_width(label) + 54)) } handle_editor_text_mouse :: proc(editor: ^Editor, view: ^SDL_View, gpu: ^GPU_Renderer, x, y: f32, extend: bool) -> bool { active := editor_active_buffer(editor) if active == nil do return false text_x := f32(editor_text_x(view)) text_y: f32 = SDL_EDITOR_TEXT_Y if x < text_x || y < text_y do return false line := view.first_line + int((y - text_y) / SDL_LINE_HEIGHT) if line < 0 || line >= buffer_line_count(&active.buffer) do return false column := text_column_from_pixel_x_gpu(gpu, &active.buffer, line, x - text_x) if extend { editor_start_selection(active) } else { editor_clear_selection(active) } cursor_move_to_line_col(&active.buffer, &active.cursor, line, max_int(column, 0)) if !extend { active.selection_anchor = active.cursor.offset } ensure_cursor_visible(editor, view) return true } scroll_project_tree :: proc(tree: ^Project_Tree, view: ^SDL_View, delta: int) { max_first := max_int(len(tree.files) - visible_tree_rows(view), 0) view.tree_first = clamp_int(view.tree_first + delta, 0, max_first) } handle_sdl_key :: proc(editor: ^Editor, view: ^SDL_View, tree: ^Project_Tree, workspace: string, completion: ^Completion_Popup, hover: ^Hover_Tooltip, definition: ^Definition_Jump, navigation: ^Navigation_History, references: ^References_Panel, rename: ^Rename_Panel, search: ^Search_Panel, diagnostics_panel: ^Diagnostics_Panel, command_palette: ^Command_Palette, tasks_panel: ^Gradle_Tasks_Panel, daemon: ^Daemon_Client, sync: ^Daemon_Sync_State, key: SDL.Keycode, mod: SDL.Keymod) -> bool { active := editor_active_buffer(editor) if active == nil do return false if command_palette.open { switch key { case SDL.K_ESCAPE: command_palette.open = false return false case SDL.K_BACKSPACE: command_palette_backspace(command_palette) return false case SDL.K_UP: command_palette.selected = max_int(command_palette.selected - 1, 0) return false case SDL.K_DOWN: command_palette.selected = min_int(command_palette.selected + 1, max_int(command_palette_match_count(command_palette) - 1, 0)) return false case SDL.K_PAGEUP: command_palette.selected = max_int(command_palette.selected - 8, 0) return false case SDL.K_PAGEDOWN: command_palette.selected = min_int(command_palette.selected + 8, max_int(command_palette_match_count(command_palette) - 1, 0)) return false case SDL.K_HOME: command_palette.selected = 0 return false case SDL.K_END: command_palette.selected = max_int(command_palette_match_count(command_palette) - 1, 0) return false case SDL.K_RETURN: command_palette_accept(command_palette, editor, view, tree, workspace, completion, hover, definition, references, rename, search, diagnostics_panel, tasks_panel, daemon, sync) return false } } if rename.open { switch key { case SDL.K_ESCAPE: rename.open = false return false case SDL.K_BACKSPACE: rename_panel_backspace(rename) return false case SDL.K_RETURN: daemon_sync_now(sync, daemon, editor, false) rename_panel_request(rename, editor, daemon) return false } } if search.open { switch key { case SDL.K_ESCAPE: search.open = false return false case SDL.K_BACKSPACE: search_panel_backspace(search) return false case SDL.K_RETURN: if (mod & SDL.KMOD_SHIFT) != SDL.KMOD_NONE { search_panel_find_previous(search, editor, view) } else { search_panel_find_next(search, editor, view) } return false } } if diagnostics_panel.open { active_for_diagnostics := editor_active_buffer(editor) diagnostic_count := 0 if active_for_diagnostics == nil else len(active_for_diagnostics.diagnostics) switch key { case SDL.K_ESCAPE: diagnostics_panel.open = false return false case SDL.K_UP: diagnostics_panel.selected = max_int(diagnostics_panel.selected - 1, 0) return false case SDL.K_DOWN: diagnostics_panel.selected = min_int(diagnostics_panel.selected + 1, max_int(diagnostic_count - 1, 0)) return false case SDL.K_PAGEUP: diagnostics_panel.selected = max_int(diagnostics_panel.selected - 7, 0) return false case SDL.K_PAGEDOWN: diagnostics_panel.selected = min_int(diagnostics_panel.selected + 7, max_int(diagnostic_count - 1, 0)) return false case SDL.K_HOME: diagnostics_panel.selected = 0 return false case SDL.K_END: diagnostics_panel.selected = max_int(diagnostic_count - 1, 0) return false case SDL.K_RETURN: diagnostics_panel_accept(diagnostics_panel, editor, view) return false } } if tasks_panel.open { switch key { case SDL.K_ESCAPE: tasks_panel.open = false view.gradle_sidebar_visible = false ui_state_save(view) return false case SDL.K_UP: tasks_panel.selected = max_int(tasks_panel.selected - 1, 0) return false case SDL.K_DOWN: tasks_panel.selected = min_int(tasks_panel.selected + 1, max_int(len(tasks_panel.items) - 1, 0)) return false case SDL.K_PAGEUP: tasks_panel.selected = max_int(tasks_panel.selected - 10, 0) return false case SDL.K_PAGEDOWN: tasks_panel.selected = min_int(tasks_panel.selected + 10, max_int(len(tasks_panel.items) - 1, 0)) return false case SDL.K_HOME: tasks_panel.selected = 0 return false case SDL.K_END: tasks_panel.selected = max_int(len(tasks_panel.items) - 1, 0) return false case SDL.K_RETURN: gradle_tasks_panel_run_selected(tasks_panel, daemon) return false } } if key == SDL.K_LEFT && (mod & SDL.KMOD_ALT) != SDL.KMOD_NONE { navigation_go_back(navigation, editor, view) return false } if key == SDL.K_RIGHT && (mod & SDL.KMOD_ALT) != SDL.KMOD_NONE { navigation_go_forward(navigation, editor, view) return false } if key == SDL.K_SPACE && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE { daemon_sync_now(sync, daemon, editor, false) completion_popup_open(completion, editor, daemon) return false } if key == SDL.K_H && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE { daemon_sync_now(sync, daemon, editor, false) hover_tooltip_open(hover, editor, daemon) return false } if key == SDL.K_B && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE { daemon_sync_now(sync, daemon, editor, false) definition_jump_request(definition, editor, daemon) return false } if key == SDL.K_R && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE && (mod & SDL.KMOD_SHIFT) != SDL.KMOD_NONE { rename_panel_open(rename) return false } if key == SDL.K_R && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE { daemon_sync_now(sync, daemon, editor, false) references_panel_request(references, editor, daemon) return false } if key == SDL.K_F && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE { search_panel_open(search) return false } if key == SDL.K_P && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE && (mod & SDL.KMOD_SHIFT) != SDL.KMOD_NONE { command_palette_open(command_palette) return false } if key == SDL.K_E && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE && (mod & SDL.KMOD_SHIFT) != SDL.KMOD_NONE { view.explorer_visible = !view.explorer_visible view.tree_first = 0 ui_state_save(view) return false } if key == SDL.K_T && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE && (mod & SDL.KMOD_SHIFT) != SDL.KMOD_NONE { if tasks_panel.open { tasks_panel.open = false view.gradle_sidebar_visible = false } else { view.gradle_sidebar_visible = true daemon_sync_now(sync, daemon, editor, false) gradle_tasks_panel_request(tasks_panel, daemon) } ui_state_save(view) return false } if key == SDL.K_M && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE && (mod & SDL.KMOD_SHIFT) != SDL.KMOD_NONE { diagnostics_panel.open = !diagnostics_panel.open diagnostics_panel.selected = 0 return false } if key == SDL.K_M && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE { view.show_metrics = !view.show_metrics return false } if key == SDL.K_F8 && (mod & SDL.KMOD_SHIFT) != SDL.KMOD_NONE { editor_jump_diagnostic(editor, view, -1) return false } if key == SDL.K_F8 { editor_jump_diagnostic(editor, view, 1) return false } if references.open { switch key { case SDL.K_ESCAPE: references.open = false return false case SDL.K_UP: references.selected = max_int(references.selected - 1, 0) return false case SDL.K_DOWN: references.selected = min_int(references.selected + 1, max_int(len(references.items) - 1, 0)) return false case SDL.K_PAGEUP: references.selected = max_int(references.selected - 14, 0) return false case SDL.K_PAGEDOWN: references.selected = min_int(references.selected + 14, max_int(len(references.items) - 1, 0)) return false case SDL.K_HOME: references.selected = 0 return false case SDL.K_END: references.selected = max_int(len(references.items) - 1, 0) return false case SDL.K_RETURN: references_panel_accept(references, navigation, editor, view) return false } } if completion.open { switch key { case SDL.K_ESCAPE: completion.open = false return false case SDL.K_UP: completion.selected = max_int(completion.selected - 1, 0) return false case SDL.K_DOWN: completion.selected = min_int(completion.selected + 1, max_int(len(completion.items) - 1, 0)) return false case SDL.K_PAGEUP: completion.selected = max_int(completion.selected - 8, 0) return false case SDL.K_PAGEDOWN: completion.selected = min_int(completion.selected + 8, max_int(len(completion.items) - 1, 0)) return false case SDL.K_HOME: completion.selected = 0 return false case SDL.K_END: completion.selected = max_int(len(completion.items) - 1, 0) return false case SDL.K_RETURN: if completion_popup_accept(completion, editor) { close_completion_accept_overlays(completion, hover, references, rename, search) ensure_cursor_visible(editor, view) daemon_sync_schedule(sync) return true } completion.open = false return false } } if key == SDL.K_S && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE && (mod & SDL.KMOD_SHIFT) != SDL.KMOD_NONE { _ = editor_save_all(editor) daemon_sync_now(sync, daemon, editor, false) return false } if key == SDL.K_S && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE { _ = editor_save_active(editor) daemon_sync_now(sync, daemon, editor, false) return false } if key == SDL.K_W && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE { if editor_close_active(editor) { scroll_sdl_view(editor, view, 0) } return false } if key == SDL.K_Z && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE { if buffer_undo(&active.buffer, &active.cursor) { editor_update_dirty(active) close_stale_edit_overlays(completion, hover, references, rename) ensure_cursor_visible(editor, view) return true } return false } if key == SDL.K_Y && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE { if buffer_redo(&active.buffer, &active.cursor) { editor_update_dirty(active) close_stale_edit_overlays(completion, hover, references, rename) ensure_cursor_visible(editor, view) return true } return false } if key == SDL.K_C && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE { editor_copy_selection_to_clipboard(active) return false } if key == SDL.K_X && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE { if editor_copy_selection_to_clipboard(active) && editor_delete_selection(active) { editor_update_dirty(active) close_stale_edit_overlays(completion, hover, references, rename) ensure_cursor_visible(editor, view) return true } return false } if key == SDL.K_V && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE { if editor_paste_clipboard(active) { close_stale_edit_overlays(completion, hover, references, rename) ensure_cursor_visible(editor, view) return true } return false } if key == SDL.K_LEFT && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE { prepare_selection_for_keyboard_move(active, mod) cursor_move_word_left(&active.buffer, &active.cursor) finish_selection_for_keyboard_move(active, mod) ensure_cursor_visible(editor, view) return false } if key == SDL.K_RIGHT && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE { prepare_selection_for_keyboard_move(active, mod) cursor_move_word_right(&active.buffer, &active.cursor) finish_selection_for_keyboard_move(active, mod) ensure_cursor_visible(editor, view) return false } switch key { case SDL.K_ESCAPE: // Window close is handled by the window manager; escape currently only clears intent. case SDL.K_RETURN: editor_insert_newline_auto_indent(active) close_stale_edit_overlays(completion, hover, references, rename) ensure_cursor_visible(editor, view) return true case SDL.K_TAB: if (mod & SDL.KMOD_SHIFT) != SDL.KMOD_NONE { if editor_outdent(active) { close_stale_edit_overlays(completion, hover, references, rename) ensure_cursor_visible(editor, view) return true } return false } editor_insert_text(active, " ") close_stale_edit_overlays(completion, hover, references, rename) ensure_cursor_visible(editor, view) return true case SDL.K_BACKSPACE: if !editor_delete_selection(active) { cursor_backspace(&active.buffer, &active.cursor) } editor_clear_selection(active) editor_update_dirty(active) close_stale_edit_overlays(completion, hover, references, rename) ensure_cursor_visible(editor, view) return true case SDL.K_DELETE: if editor_delete_selection(active) || cursor_delete_forward(&active.buffer, &active.cursor) { editor_clear_selection(active) editor_update_dirty(active) close_stale_edit_overlays(completion, hover, references, rename) ensure_cursor_visible(editor, view) return true } case SDL.K_UP: prepare_selection_for_keyboard_move(active, mod) cursor_move_vertical(&active.buffer, &active.cursor, -1) finish_selection_for_keyboard_move(active, mod) ensure_cursor_visible(editor, view) case SDL.K_DOWN: prepare_selection_for_keyboard_move(active, mod) cursor_move_vertical(&active.buffer, &active.cursor, 1) finish_selection_for_keyboard_move(active, mod) ensure_cursor_visible(editor, view) case SDL.K_LEFT: prepare_selection_for_keyboard_move(active, mod) if active.cursor.offset > 0 { active.cursor.offset -= 1 _, active.cursor.wanted_column = buffer_offset_to_line_col(&active.buffer, active.cursor.offset) } finish_selection_for_keyboard_move(active, mod) case SDL.K_RIGHT: prepare_selection_for_keyboard_move(active, mod) if active.cursor.offset < buffer_len(&active.buffer) { active.cursor.offset += 1 _, active.cursor.wanted_column = buffer_offset_to_line_col(&active.buffer, active.cursor.offset) } finish_selection_for_keyboard_move(active, mod) case SDL.K_HOME: prepare_selection_for_keyboard_move(active, mod) line, _ := buffer_offset_to_line_col(&active.buffer, active.cursor.offset) active.cursor.offset = buffer_line_start(&active.buffer, line) active.cursor.wanted_column = 0 finish_selection_for_keyboard_move(active, mod) ensure_cursor_visible(editor, view) case SDL.K_END: prepare_selection_for_keyboard_move(active, mod) line, _ := buffer_offset_to_line_col(&active.buffer, active.cursor.offset) active.cursor.offset = buffer_line_end(&active.buffer, line) _, active.cursor.wanted_column = buffer_offset_to_line_col(&active.buffer, active.cursor.offset) finish_selection_for_keyboard_move(active, mod) ensure_cursor_visible(editor, view) case SDL.K_PAGEUP: prepare_selection_for_keyboard_move(active, mod) page_delta := max_int(visible_editor_lines(view) - 2, 1) cursor_move_vertical(&active.buffer, &active.cursor, -page_delta) finish_selection_for_keyboard_move(active, mod) scroll_sdl_view(editor, view, -page_delta) ensure_cursor_visible(editor, view) case SDL.K_PAGEDOWN: prepare_selection_for_keyboard_move(active, mod) page_delta := max_int(visible_editor_lines(view) - 2, 1) cursor_move_vertical(&active.buffer, &active.cursor, page_delta) finish_selection_for_keyboard_move(active, mod) scroll_sdl_view(editor, view, page_delta) ensure_cursor_visible(editor, view) } return false } editor_copy_selection_to_clipboard :: proc(active: ^Editor_Buffer) -> bool { start, end, ok := editor_selection_range(active) if !ok do return false bytes := buffer_range_bytes(&active.buffer, start, end - start) defer delete(bytes) text := string(bytes[:]) c_text, err := strings.clone_to_cstring(text, context.temp_allocator) if err != nil do return false return SDL.SetClipboardText(c_text) } editor_paste_clipboard :: proc(active: ^Editor_Buffer) -> bool { if !SDL.HasClipboardText() do return false raw := SDL.GetClipboardText() if raw == nil do return false defer SDL.free(raw) text := string(cstring(raw)) if len(text) == 0 do return false editor_insert_text(active, text) return true } editor_jump_diagnostic :: proc(editor: ^Editor, view: ^SDL_View, direction: int) { active := editor_active_buffer(editor) if active == nil || len(active.diagnostics) == 0 do return cursor_line, cursor_column := buffer_offset_to_line_col(&active.buffer, active.cursor.offset) best_index := -1 best_distance := 1 << 30 for diagnostic, index in active.diagnostics { distance: int if direction >= 0 { if diagnostic.line > cursor_line || (diagnostic.line == cursor_line && diagnostic.column > cursor_column) { distance = (diagnostic.line - cursor_line) * 100000 + diagnostic.column - cursor_column } else { distance = (buffer_line_count(&active.buffer) + diagnostic.line - cursor_line) * 100000 + diagnostic.column } } else { if diagnostic.line < cursor_line || (diagnostic.line == cursor_line && diagnostic.column < cursor_column) { distance = (cursor_line - diagnostic.line) * 100000 + cursor_column - diagnostic.column } else { distance = (buffer_line_count(&active.buffer) + cursor_line - diagnostic.line) * 100000 + cursor_column } } if distance < best_distance { best_distance = distance best_index = index } } if best_index < 0 do return target := active.diagnostics[best_index] cursor_move_to_line_col(&active.buffer, &active.cursor, target.line, target.column) editor_clear_selection(active) ensure_cursor_visible(editor, view) } diagnostics_panel_accept :: proc(panel: ^Diagnostics_Panel, editor: ^Editor, view: ^SDL_View) { active := editor_active_buffer(editor) if active == nil || len(active.diagnostics) == 0 do return index := clamp_int(panel.selected, 0, len(active.diagnostics) - 1) diagnostic := active.diagnostics[index] cursor_move_to_line_col(&active.buffer, &active.cursor, diagnostic.line, diagnostic.column) editor_clear_selection(active) ensure_cursor_visible(editor, view) } prepare_selection_for_keyboard_move :: proc(active: ^Editor_Buffer, mod: SDL.Keymod) { if (mod & SDL.KMOD_SHIFT) != SDL.KMOD_NONE { editor_start_selection(active) } } finish_selection_for_keyboard_move :: proc(active: ^Editor_Buffer, mod: SDL.Keymod) { if (mod & SDL.KMOD_SHIFT) != SDL.KMOD_NONE { start, end, ok := editor_selection_range(active) if !ok || start == end { editor_clear_selection(active) } } else { editor_clear_selection(active) } } scroll_sdl_view :: proc(editor: ^Editor, view: ^SDL_View, delta: int) { active := editor_active_buffer(editor) if active == nil do return max_first := max_int(buffer_line_count(&active.buffer) - visible_editor_lines(view), 0) view.first_line = clamp_int(view.first_line + delta, 0, max_first) } ensure_cursor_visible :: proc(editor: ^Editor, view: ^SDL_View) { active := editor_active_buffer(editor) if active == nil do return line, _ := buffer_offset_to_line_col(&active.buffer, active.cursor.offset) visible_lines := visible_editor_lines(view) if line < view.first_line { view.first_line = line } else if line >= view.first_line + visible_lines { view.first_line = line - visible_lines + 1 } scroll_sdl_view(editor, view, 0) } refresh_view_size :: proc(window: ^SDL.Window, view: ^SDL_View) { w, h: i32 if SDL.GetWindowSize(window, &w, &h) { view.window_width = max_int(int(w), 1) view.window_height = max_int(int(h), 1) } } visible_editor_lines :: proc(view: ^SDL_View) -> int { available := view.window_height - SDL_EDITOR_TEXT_Y - SDL_STATUS_BAR_HEIGHT - 8 return max_int(available / SDL_LINE_HEIGHT, 1) } visible_tree_rows :: proc(view: ^SDL_View) -> int { available := view.window_height - SDL_TREE_FIRST_Y - SDL_STATUS_BAR_HEIGHT - 8 return max_int(available / SDL_TREE_ROW_HEIGHT, 1) } render_sdl_editor :: proc(renderer: ^SDL.Renderer, editor: ^Editor, view: ^SDL_View, tree: ^Project_Tree, completion: ^Completion_Popup, hover: ^Hover_Tooltip, references: ^References_Panel, rename: ^Rename_Panel) { active := editor_active_buffer(editor) if active == nil do return _ = SDL.SetRenderDrawColor(renderer, 15, 17, 22, 255) _ = SDL.RenderClear(renderer) render_project_tree(renderer, tree, view, active.path) _ = SDL.SetRenderDrawColor(renderer, 37, 41, 54, 255) sidebar_width := left_sidebar_width(view) text_x := editor_text_x(view) gutter := SDL.FRect{f32(sidebar_width), 0, 72, f32(view.window_height)} _ = SDL.RenderFillRect(renderer, &gutter) cursor_line, cursor_col := buffer_offset_to_line_col(&active.buffer, active.cursor.offset) line_height: f32 = 16 y: f32 = 18 dirty_marker := "" if !active.dirty else "*" _ = SDL.SetRenderDrawColor(renderer, 180, 190, 210, 255) title := fmt.tprintf("%s%s v%d", active.path, dirty_marker, active.buffer.version) render_debug_text_limited(renderer, f32(sidebar_width + 84), 4, title, 92) for screen_line := 0; screen_line < visible_editor_lines(view); screen_line += 1 { line_index := view.first_line + screen_line if line_index >= buffer_line_count(&active.buffer) do break bytes := buffer_line_bytes(&active.buffer, line_index) line_text := string(bytes[:]) gutter_text := fmt.ctprintf("%4d", line_index + 1) diagnostic, has_diagnostic := editor_diagnostic_on_line(editor, line_index) if line_index == cursor_line { marker := SDL.FRect{f32(sidebar_width), y - 2, f32(view.window_width - sidebar_width), line_height} _ = SDL.SetRenderDrawColor(renderer, 29, 34, 48, 255) _ = SDL.RenderFillRect(renderer, &marker) _ = SDL.SetRenderDrawColor(renderer, 138, 180, 248, 255) } else if has_diagnostic { _ = SDL.SetRenderDrawColor(renderer, 235, 95, 95, 255) } else { _ = SDL.SetRenderDrawColor(renderer, 103, 111, 135, 255) } _ = SDL.RenderDebugText(renderer, f32(sidebar_width + 20), y, gutter_text) if has_diagnostic { _ = SDL.RenderDebugText(renderer, f32(sidebar_width + 62), y, "!") } render_selection_for_line(renderer, view, active, line_index, y) _ = SDL.SetRenderDrawColor(renderer, 220, 225, 235, 255) render_debug_text_limited(renderer, f32(text_x), y, line_text, visible_text_columns(view)) if line_index == cursor_line { cursor_x := f32(text_x + cursor_col * SDL.DEBUG_TEXT_FONT_CHARACTER_SIZE) _ = SDL.SetRenderDrawColor(renderer, 255, 210, 120, 255) _ = SDL.RenderLine(renderer, cursor_x, y - 2, cursor_x, y + line_height - 2) } delete(bytes) y += line_height } if len(editor.status) > 0 { _ = SDL.SetRenderDrawColor(renderer, 145, 165, 205, 255) status := fmt.ctprintf("%s", editor.status) _ = SDL.RenderDebugText(renderer, f32(sidebar_width + 84), 734, status) } else if len(active.diagnostics) > 0 { first := active.diagnostics[0] errors, warnings, infos := editor_diagnostic_counts(active) status := fmt.tprintf("%d errors %d warnings %d info | %s %d:%d %s", errors, warnings, infos, first.severity, first.line + 1, first.column + 1, first.message) _ = SDL.SetRenderDrawColor(renderer, 235, 95, 95, 255) render_debug_text_limited(renderer, f32(sidebar_width + 84), 734, status, 92) } else if active.dirty { _ = SDL.SetRenderDrawColor(renderer, 255, 210, 120, 255) _ = SDL.RenderDebugText(renderer, f32(sidebar_width + 84), 734, "modified - Ctrl+S to save") } else { _ = SDL.SetRenderDrawColor(renderer, 120, 210, 150, 255) _ = SDL.RenderDebugText(renderer, f32(sidebar_width + 84), 734, "saved") } render_completion_popup(renderer, editor, view, completion) render_hover_tooltip(renderer, editor, view, hover) render_references_panel(renderer, references) render_rename_panel(renderer, rename) _ = SDL.RenderPresent(renderer) } render_sdl_editor_gpu :: proc(gpu: ^GPU_Renderer, editor: ^Editor, view: ^SDL_View, tree: ^Project_Tree, completion: ^Completion_Popup, hover: ^Hover_Tooltip, references: ^References_Panel, rename: ^Rename_Panel, search: ^Search_Panel, diagnostics_panel: ^Diagnostics_Panel, command_palette: ^Command_Palette, tasks_panel: ^Gradle_Tasks_Panel) { active := editor_active_buffer(editor) if active == nil do return gpu_begin(gpu) gpu_rect(gpu, 0, 0, f32(gpu.width), f32(gpu.height), 30, 31, 34, 255) render_top_bar_gpu(gpu, active) sidebar_width := left_sidebar_width(view) text_x := editor_text_x(view) render_project_tree_gpu(gpu, tree, view, active.path) editor_height := f32(gpu.height - SDL_EDITOR_TOP - SDL_STATUS_BAR_HEIGHT) gpu_rect(gpu, f32(sidebar_width), SDL_TOP_BAR_HEIGHT, f32(gpu.width - sidebar_width), SDL_TAB_BAR_HEIGHT, 37, 38, 43, 255) gpu_rect(gpu, f32(sidebar_width), SDL_EDITOR_TOP, f32(SDL_GUTTER_WIDTH), editor_height, 32, 33, 36, 255) gpu_rect(gpu, f32(sidebar_width + SDL_GUTTER_WIDTH), SDL_EDITOR_TOP, f32(gpu.width - sidebar_width - SDL_GUTTER_WIDTH), editor_height, 27, 28, 32, 255) gpu_rect(gpu, f32(sidebar_width - 1), SDL_TOP_BAR_HEIGHT, 1, f32(gpu.height - SDL_TOP_BAR_HEIGHT), 48, 50, 56, 255) gpu_rect(gpu, f32(sidebar_width + SDL_GUTTER_WIDTH - 1), SDL_EDITOR_TOP, 1, editor_height, 43, 45, 51, 255) render_editor_tabs_gpu(gpu, editor, view) cursor_line, cursor_col := buffer_offset_to_line_col(&active.buffer, active.cursor.offset) line_height: f32 = SDL_LINE_HEIGHT y: f32 = SDL_EDITOR_TEXT_Y syntax_state := kotlin_syntax_state_before_line(&active.buffer, view.first_line) for screen_line := 0; screen_line < visible_editor_lines(view); screen_line += 1 { line_index := view.first_line + screen_line if line_index >= buffer_line_count(&active.buffer) do break bytes := buffer_line_bytes(&active.buffer, line_index) line_text := string(bytes[:]) gutter_text := fmt.tprintf("%4d", line_index + 1) diagnostic, has_diagnostic := editor_diagnostic_on_line(editor, line_index) gutter_color := [4]u8{116, 120, 128, 255} if line_index == cursor_line { gpu_rect(gpu, f32(sidebar_width), y - 3, f32(gpu.width - sidebar_width), line_height + 1, 38, 40, 46, 255) gpu_rect(gpu, f32(sidebar_width + SDL_GUTTER_WIDTH), y - 3, 2, line_height + 1, 77, 155, 230, 255) gutter_color = {169, 174, 184, 255} } else if has_diagnostic { gutter_color = {244, 113, 116, 255} } gpu_text(gpu, f32(sidebar_width + 18), y, gutter_text, gutter_color[0], gutter_color[1], gutter_color[2], gutter_color[3]) if has_diagnostic { gpu_text(gpu, f32(sidebar_width + 58), y, "!", 244, 113, 116, 255) } render_selection_for_line_gpu(gpu, view, active, line_index, y) render_search_matches_for_line_gpu(gpu, view, active, search, line_index, line_text, y) syntax_state = render_syntax_line_gpu(gpu, f32(text_x), y, line_text, visible_gpu_text_columns(gpu, view, tasks_panel), syntax_state) if has_diagnostic { render_diagnostic_underline_gpu(gpu, view, &active.buffer, line_index, diagnostic.column, y) } if line_index == cursor_line { cursor_x := editor_column_pixel_x_gpu(gpu, view, &active.buffer, cursor_line, cursor_col) gpu_line(gpu, cursor_x, y - 3, cursor_x, y + line_height - 1, 230, 230, 230, 255) } delete(bytes) y += line_height } if len(editor.status) > 0 { render_status_bar_gpu(gpu, editor.status, false) } else if len(active.diagnostics) > 0 { first := active.diagnostics[0] errors, warnings, infos := editor_diagnostic_counts(active) status := fmt.tprintf("%d errors %d warnings %d info | %s %d:%d %s", errors, warnings, infos, first.severity, first.line + 1, first.column + 1, first.message) render_status_bar_gpu(gpu, status, true) } else if active.dirty { render_status_bar_gpu(gpu, "modified - Ctrl+S to save", false) } else { render_status_bar_gpu(gpu, "saved", false) } render_completion_popup_gpu(gpu, editor, view, completion) render_hover_tooltip_gpu(gpu, editor, view, hover) render_references_panel_gpu(gpu, references) render_rename_panel_gpu(gpu, rename) render_search_panel_gpu(gpu, search) render_diagnostics_panel_gpu(gpu, editor, view, diagnostics_panel) render_gradle_tasks_panel_gpu(gpu, view, tasks_panel) render_command_palette_gpu(gpu, command_palette) render_metrics_overlay_gpu(gpu, editor, view) gpu_present(gpu) } visible_text_columns :: proc(view: ^SDL_View) -> int { return max_int((view.window_width - editor_text_x(view) - 8) / SDL.DEBUG_TEXT_FONT_CHARACTER_SIZE, 1) } visible_gpu_text_columns :: proc(gpu: ^GPU_Renderer, view: ^SDL_View, tasks_panel: ^Gradle_Tasks_Panel) -> int { right_limit := gpu.width - 16 if tasks_panel != nil && tasks_panel.open { right_limit = gpu.width - right_sidebar_width_view(view, gpu.width) - 16 } return max_int(int(f32(right_limit - editor_text_x(view)) / max_f32(gpu.font_advance, 1)), 1) } editor_column_pixel_x :: proc(view: ^SDL_View, buffer: ^Buffer, line, column: int) -> f32 { return f32(editor_text_x(view) + line_prefix_pixel_width(buffer, line, column)) } editor_column_pixel_x_gpu :: proc(gpu: ^GPU_Renderer, view: ^SDL_View, buffer: ^Buffer, line, column: int) -> f32 { return round_f32(f32(editor_text_x(view)) + line_prefix_pixel_advance_gpu(gpu, buffer, line, column)) } line_prefix_pixel_width :: proc(buffer: ^Buffer, line, column: int) -> int { bytes := buffer_line_bytes(buffer, line) defer delete(bytes) prefix_len := clamp_int(column, 0, len(bytes)) return gpu_text_width(string(bytes[:prefix_len])) } line_prefix_pixel_width_gpu :: proc(gpu: ^GPU_Renderer, buffer: ^Buffer, line, column: int) -> int { return int(line_prefix_pixel_advance_gpu(gpu, buffer, line, column) + 0.5) } line_prefix_pixel_advance_gpu :: proc(gpu: ^GPU_Renderer, buffer: ^Buffer, line, column: int) -> f32 { bytes := buffer_line_bytes(buffer, line) defer delete(bytes) prefix_len := clamp_int(column, 0, len(bytes)) return gpu_font_text_advance(gpu, string(bytes[:prefix_len])) } text_column_from_pixel_x :: proc(buffer: ^Buffer, line: int, x: f32) -> int { bytes := buffer_line_bytes(buffer, line) defer delete(bytes) if len(bytes) == 0 do return 0 target := max_int(int(x), 0) best := 0 best_distance := 1 << 30 for column := 0; column <= len(bytes); column += 1 { width := gpu_text_width(string(bytes[:column])) distance := abs_int(width - target) if distance < best_distance { best = column best_distance = distance } if width > target && column > 0 do break } return best } text_column_from_pixel_x_gpu :: proc(gpu: ^GPU_Renderer, buffer: ^Buffer, line: int, x: f32) -> int { if gpu == nil || !gpu.available || gpu.font_advance <= 0 { return text_column_from_pixel_x(buffer, line, x) } bytes := buffer_line_bytes(buffer, line) defer delete(bytes) if len(bytes) == 0 do return 0 column := int((x + gpu.font_advance * 0.5) / gpu.font_advance) return clamp_int(column, 0, len(bytes)) } abs_int :: proc(v: int) -> int { if v < 0 do return -v return v } render_syntax_line_gpu :: proc(gpu: ^GPU_Renderer, x, y: f32, line: string, max_chars: int, state: Syntax_State) -> Syntax_State { if len(line) == 0 do return state limit := min_int(len(line), max_chars) spans, next_state := tokenize_kotlin_line(line[:limit], state) defer delete(spans) for span in spans { if span.start >= span.end do continue text := line[span.start:span.end] offset := gpu_font_text_advance(gpu, line[:span.start]) r, g, b := syntax_color(span.kind) gpu_text(gpu, round_f32(x + offset), y, text, r, g, b, 255) } return next_state } tokenize_kotlin_line :: proc(line: string, initial_state: Syntax_State) -> ([dynamic]Syntax_Span, Syntax_State) { spans: [dynamic]Syntax_Span state := initial_state i := 0 for i < len(line) { start := i ch := line[i] if state.in_block_comment { for i < len(line) { if line[i] == '*' && i + 1 < len(line) && line[i + 1] == '/' { i += 2 state.in_block_comment = false break } i += 1 } append(&spans, Syntax_Span{start = start, end = i, kind = .Comment}) continue } if state.in_triple_string { for i < len(line) { if starts_with_at(line, i, "\"\"\"") { i += 3 state.in_triple_string = false break } i += 1 } append(&spans, Syntax_Span{start = start, end = i, kind = .String}) continue } if ch == '/' && i + 1 < len(line) && line[i + 1] == '/' { append(&spans, Syntax_Span{start = i, end = len(line), kind = .Comment}) break } if ch == '/' && i + 1 < len(line) && line[i + 1] == '*' { i += 2 state.in_block_comment = true for i < len(line) { if line[i] == '*' && i + 1 < len(line) && line[i + 1] == '/' { i += 2 state.in_block_comment = false break } i += 1 } append(&spans, Syntax_Span{start = start, end = i, kind = .Comment}) continue } if starts_with_at(line, i, "\"\"\"") { i += 3 state.in_triple_string = true for i < len(line) { if starts_with_at(line, i, "\"\"\"") { i += 3 state.in_triple_string = false break } i += 1 } append(&spans, Syntax_Span{start = start, end = i, kind = .String}) continue } if ch == '"' { i += 1 escaped := false for i < len(line) { if line[i] == '"' && !escaped { i += 1 break } escaped = line[i] == '\\' && !escaped if line[i] != '\\' do escaped = false i += 1 } append(&spans, Syntax_Span{start = start, end = i, kind = .String}) continue } if ch == '\'' { i += 1 escaped := false for i < len(line) { if line[i] == '\'' && !escaped { i += 1 break } escaped = line[i] == '\\' && !escaped if line[i] != '\\' do escaped = false i += 1 } append(&spans, Syntax_Span{start = start, end = i, kind = .String}) continue } if is_digit_byte(ch) { i += 1 for i < len(line) && (is_digit_byte(line[i]) || line[i] == '.') { i += 1 } append(&spans, Syntax_Span{start = start, end = i, kind = .Number}) continue } if is_identifier_start_byte(ch) { i += 1 for i < len(line) && is_identifier_part_byte(line[i]) { i += 1 } word := line[start:i] kind := Syntax_Kind.Keyword if is_kotlin_or_java_keyword(word) else Syntax_Kind.Plain if kind == .Plain && is_type_like_identifier(word) { kind = .Type } append(&spans, Syntax_Span{start = start, end = i, kind = kind}) continue } i += 1 for i < len(line) && !is_identifier_start_byte(line[i]) && !is_digit_byte(line[i]) && line[i] != '"' && line[i] != '\'' { if line[i] == '/' && i + 1 < len(line) && line[i + 1] == '/' do break i += 1 } append(&spans, Syntax_Span{start = start, end = i, kind = .Plain}) } return spans, state } kotlin_syntax_state_before_line :: proc(buffer: ^Buffer, line: int) -> Syntax_State { state := Syntax_State{} for index := 0; index < line; index += 1 { bytes := buffer_line_bytes(buffer, index) spans: [dynamic]Syntax_Span spans, state = tokenize_kotlin_line(string(bytes[:]), state) delete(spans) delete(bytes) } return state } starts_with_at :: proc(text: string, at: int, prefix: string) -> bool { if at < 0 || at + len(prefix) > len(text) do return false return text[at:at + len(prefix)] == prefix } syntax_color :: proc(kind: Syntax_Kind) -> (u8, u8, u8) { switch kind { case .Keyword: return 197, 134, 192 case .String: return 206, 145, 120 case .Comment: return 106, 153, 85 case .Number: return 181, 206, 168 case .Type: return 78, 201, 176 case .Plain: return 214, 217, 223 } return 214, 217, 223 } is_kotlin_or_java_keyword :: proc(word: string) -> bool { switch word { case "as", "break", "class", "continue", "do", "else", "false", "for", "fun", "if", "in", "interface", "is", "null", "object", "package", "return", "super", "this", "throw", "true", "try", "typealias", "typeof", "val", "var", "when", "while", "by", "catch", "constructor", "delegate", "dynamic", "field", "file", "finally", "get", "import", "init", "param", "property", "receiver", "set", "setparam", "where", "actual", "abstract", "annotation", "companion", "const", "crossinline", "data", "enum", "expect", "external", "final", "infix", "inline", "inner", "internal", "lateinit", "noinline", "open", "operator", "out", "override", "private", "protected", "public", "reified", "sealed", "suspend", "tailrec", "vararg": return true case "assert", "boolean", "byte", "case", "char", "default", "double", "extends", "float", "goto", "implements", "instanceof", "int", "long", "native", "new", "record", "short", "static", "strictfp", "switch", "synchronized", "throws", "transient", "void", "volatile": return true } return false } is_type_like_identifier :: proc(word: string) -> bool { if len(word) == 0 do return false return word[0] >= 'A' && word[0] <= 'Z' } is_identifier_start_byte :: proc(ch: u8) -> bool { return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_' } is_identifier_part_byte :: proc(ch: u8) -> bool { return is_identifier_start_byte(ch) || is_digit_byte(ch) } is_digit_byte :: proc(ch: u8) -> bool { return ch >= '0' && ch <= '9' } render_debug_text_limited :: proc(renderer: ^SDL.Renderer, x, y: f32, text: string, max_chars: int) { if max_chars <= 0 do return if len(text) <= max_chars { text_c := fmt.ctprintf("%s", text) _ = SDL.RenderDebugText(renderer, x, y, text_c) return } if max_chars <= 3 { text_c := fmt.ctprintf("%s", text[:max_chars]) _ = SDL.RenderDebugText(renderer, x, y, text_c) return } text_c := fmt.ctprintf("%s...", text[:max_chars - 3]) _ = SDL.RenderDebugText(renderer, x, y, text_c) } render_rename_panel :: proc(renderer: ^SDL.Renderer, panel: ^Rename_Panel) { if !panel.open do return x: f32 = 620 y: f32 = 420 width: f32 = 460 visible := min_int(max_int(len(panel.edits), 1), 8) height := f32(76 + visible * 16) _ = SDL.SetRenderDrawColor(renderer, 24, 28, 38, 245) rect := SDL.FRect{x, y, width, height} _ = SDL.RenderFillRect(renderer, &rect) _ = SDL.SetRenderDrawColor(renderer, 110, 135, 180, 255) _ = SDL.RenderRect(renderer, &rect) _ = SDL.SetRenderDrawColor(renderer, 145, 165, 205, 255) _ = SDL.RenderDebugText(renderer, x + 10, y + 8, "Rename Preview") input := fmt.ctprintf("New name: %s", string(panel.input[:])) _ = SDL.SetRenderDrawColor(renderer, 255, 210, 120, 255) _ = SDL.RenderDebugText(renderer, x + 10, y + 28, input) summary := fmt.ctprintf("%s", panel.summary) _ = SDL.SetRenderDrawColor(renderer, 220, 225, 235, 255) _ = SDL.RenderDebugText(renderer, x + 10, y + 48, summary) item_y := y + 68 for edit, index in panel.edits { if index >= 8 do break label := fmt.ctprintf("%s", edit) _ = SDL.RenderDebugText(renderer, x + 10, item_y, label) item_y += 16 } } render_rename_panel_gpu :: proc(gpu: ^GPU_Renderer, panel: ^Rename_Panel) { if !panel.open do return x: f32 = 620 y: f32 = 420 width: f32 = 460 visible := min_int(max_int(len(panel.edits), 1), 8) height := f32(76 + visible * 16) gpu_rect(gpu, x, y, width, height, 24, 28, 38, 245) gpu_rect_outline(gpu, x, y, width, height, 110, 135, 180, 255) gpu_text(gpu, x + 10, y + 8, "Rename Preview", 145, 165, 205, 255) input := fmt.tprintf("New name: %s", string(panel.input[:])) gpu_text_limited(gpu, x + 10, y + 28, input, 56, 255, 210, 120, 255) gpu_text_limited(gpu, x + 10, y + 48, panel.summary, 56, 220, 225, 235, 255) item_y := y + 68 for edit, index in panel.edits { if index >= 8 do break gpu_text_limited(gpu, x + 10, item_y, edit, 56, 220, 225, 235, 255) item_y += 16 } } render_search_panel_gpu :: proc(gpu: ^GPU_Renderer, panel: ^Search_Panel) { if !panel.open do return width: f32 = 440 height: f32 = 72 x := f32(gpu.width) - width - 24 y: f32 = SDL_TOP_BAR_HEIGHT + SDL_TAB_BAR_HEIGHT + 12 gpu_rect(gpu, x, y, width, height, 31, 34, 41, 245) gpu_rect_outline(gpu, x, y, width, height, 77, 155, 230, 255) gpu_text(gpu, x + 12, y + 10, "Find", 145, 190, 255, 255) query := fmt.tprintf("%s", string(panel.input[:])) gpu_rect(gpu, x + 54, y + 8, width - 66, 24, 24, 26, 31, 255) gpu_rect_outline(gpu, x + 54, y + 8, width - 66, 24, 64, 68, 78, 255) gpu_text_limited(gpu, x + 62, y + 15, query, 48, 230, 233, 238, 255) if len(panel.message) > 0 { gpu_text_limited(gpu, x + 12, y + 46, panel.message, 58, 170, 176, 186, 255) } } render_command_palette_gpu :: proc(gpu: ^GPU_Renderer, palette: ^Command_Palette) { if !palette.open do return width := f32(min_int(max_int(gpu.width - 280, 420), 720)) x := f32(gpu.width) * 0.5 - width * 0.5 y: f32 = SDL_TOP_BAR_HEIGHT + 48 visible := min_int(max_int(command_palette_match_count(palette), 1), 8) height := f32(58 + visible * 24) gpu_rect(gpu, x, y, width, height, 25, 28, 36, 248) gpu_rect_outline(gpu, x, y, width, height, 77, 155, 230, 255) gpu_text(gpu, x + 16, y + 13, "Command Palette", 145, 190, 255, 255) query := fmt.tprintf(">%s", string(palette.input[:])) gpu_rect(gpu, x + 14, y + 34, width - 28, 24, 18, 20, 25, 255) gpu_rect_outline(gpu, x + 14, y + 34, width - 28, 24, 58, 62, 72, 255) gpu_text_limited(gpu, x + 24, y + 41, query, max_int(int((width - 48) / max_f32(gpu.font_advance, 1)), 1), 230, 233, 238, 255) item_y := y + 68 seen := 0 rendered := 0 first_seen := 0 if palette.selected >= 8 { first_seen = palette.selected - 7 } for item in COMMAND_ITEMS { if !command_palette_matches(palette, item.label) do continue if seen < first_seen { seen += 1 continue } if rendered >= 8 do break if seen == palette.selected { gpu_rect(gpu, x + 8, item_y - 5, width - 16, 22, 49, 56, 70, 255) gpu_rect(gpu, x + 8, item_y - 5, 2, 22, 77, 155, 230, 255) } color := [3]u8{218, 222, 230} if seen != palette.selected { color = {166, 172, 184} } gpu_text_limited(gpu, x + 20, item_y, item.label, max_int(int((width - 40) / max_f32(gpu.font_advance, 1)), 1), color[0], color[1], color[2], 255) item_y += 24 seen += 1 rendered += 1 } if rendered == 0 { gpu_text(gpu, x + 20, item_y, "No matching commands", 150, 154, 162, 255) } } render_diagnostics_panel_gpu :: proc(gpu: ^GPU_Renderer, editor: ^Editor, view: ^SDL_View, panel: ^Diagnostics_Panel) { if !panel.open do return active := editor_active_buffer(editor) if active == nil do return height: f32 = 168 sidebar_width := left_sidebar_width(view) x := f32(sidebar_width) y := f32(gpu.height - SDL_STATUS_BAR_HEIGHT) - height width := f32(gpu.width - sidebar_width) gpu_rect(gpu, x, y, width, height, 30, 32, 38, 245) gpu_rect(gpu, x, y, width, 1, 64, 68, 78, 255) gpu_text(gpu, x + 14, y + 10, "Diagnostics", 220, 223, 228, 255) errors, warnings, infos := editor_diagnostic_counts(active) count_label := fmt.tprintf("%dE %dW %dI", errors, warnings, infos) gpu_text(gpu, x + width - 120, y + 10, count_label, 150, 154, 162, 255) if len(active.diagnostics) == 0 { gpu_text(gpu, x + 14, y + 42, "No diagnostics", 150, 154, 162, 255) return } panel.selected = clamp_int(panel.selected, 0, len(active.diagnostics) - 1) visible_items := 7 first_index := 0 if panel.selected >= visible_items { first_index = panel.selected - visible_items + 1 } item_y := y + 38 for index := first_index; index < len(active.diagnostics) && index < first_index + visible_items; index += 1 { diagnostic := active.diagnostics[index] if index == panel.selected { gpu_rect(gpu, x + 8, item_y - 4, width - 16, 20, 49, 56, 70, 255) gpu_rect(gpu, x + 8, item_y - 4, 2, 20, 77, 155, 230, 255) } label := fmt.tprintf("%s %d:%d %s", diagnostic.severity, diagnostic.line + 1, diagnostic.column + 1, diagnostic.message) color := [3]u8{218, 222, 230} if diagnostic.severity == "error" { color = {244, 113, 116} } gpu_text_limited(gpu, x + 18, item_y, label, max_int((gpu.width - sidebar_width - 40) / int(max_f32(gpu.font_advance, 1)), 1), color[0], color[1], color[2], 255) item_y += 18 } } render_gradle_tasks_panel_gpu :: proc(gpu: ^GPU_Renderer, view: ^SDL_View, panel: ^Gradle_Tasks_Panel) { if !panel.open do return width := f32(right_sidebar_width_view(view, gpu.width)) height := f32(gpu.height - SDL_TOP_BAR_HEIGHT - SDL_STATUS_BAR_HEIGHT) x := f32(gpu.width) - width y: f32 = SDL_TOP_BAR_HEIGHT gpu_rect(gpu, x, y, width, height, 35, 36, 40, 255) gpu_rect(gpu, x, y, 1, height, 56, 58, 64, 255) gpu_rect(gpu, x, y + SDL_TAB_BAR_HEIGHT, width, 1, 52, 54, 60, 255) gpu_text(gpu, x + 14, y + 12, "GRADLE", 150, 154, 162, 255) gpu_text(gpu, x + 14, y + 34, "Tasks", 220, 223, 228, 255) task_count := fmt.tprintf("%d", len(panel.items)) gpu_text_limited(gpu, x + width - 54, y + 34, task_count, 5, 150, 154, 162, 255) if len(panel.message) > 0 { gpu_text_limited(gpu, x + 14, y + 58, panel.message, 38, 150, 154, 162, 255) } if len(panel.items) == 0 { gpu_text_limited(gpu, x + 14, y + 92, "No Gradle tasks loaded", 34, 150, 154, 162, 255) } item_y := y + 92 output_height: f32 = 0 if len(panel.output) > 0 { output_height = min_f32(160, max_f32(72, height * 0.32)) } visible_items := max_int(int((height - output_height - 104) / 18), 1) if len(panel.items) > 0 { panel.selected = clamp_int(panel.selected, 0, len(panel.items) - 1) } first_index := 0 if panel.selected >= visible_items { first_index = panel.selected - visible_items + 1 } for index := first_index; index < len(panel.items) && index < first_index + visible_items; index += 1 { item := panel.items[index] if index == panel.selected { gpu_rect(gpu, x + 8, item_y - 4, width - 16, 20, 49, 56, 70, 255) gpu_rect(gpu, x + 8, item_y - 4, 2, 20, 77, 155, 230, 255) } label := item.path if len(label) == 0 do label = item.name gpu_text_limited(gpu, x + 18, item_y, label, 34, 218, 222, 230, 255) if len(item.description) > 0 { gpu_text_limited(gpu, x + 36, item_y + 13, item.description, 32, 150, 154, 162, 255) } item_y += 30 if len(item.description) > 0 else 18 } if len(panel.output) > 0 { output_y := y + height - output_height gpu_rect(gpu, x, output_y, width, output_height, 28, 29, 34, 255) gpu_rect(gpu, x, output_y, width, 1, 58, 61, 68, 255) gpu_text(gpu, x + 14, output_y + 10, "Output", 150, 154, 162, 255) line_y := output_y + 34 max_lines := max_int(int((output_height - 42) / 16), 1) first := max_int(len(panel.output) - max_lines, 0) max_columns := max_int(int((width - 28) / max_f32(gpu.font_advance, 1)), 1) for index := first; index < len(panel.output); index += 1 { line := panel.output[index] color := [3]u8{176, 182, 192} if strings.has_prefix(line, "stderr:") { color = {244, 113, 116} } gpu_text_limited(gpu, x + 14, line_y, line, max_columns, color[0], color[1], color[2], 255) line_y += 16 } } } render_references_panel :: proc(renderer: ^SDL.Renderer, panel: ^References_Panel) { if !panel.open do return x: f32 = 700 y: f32 = 72 width: f32 = 380 visible := min_int(max_int(len(panel.items), 1), 14) height := f32(30 + visible * 16) _ = SDL.SetRenderDrawColor(renderer, 24, 28, 38, 245) rect := SDL.FRect{x, y, width, height} _ = SDL.RenderFillRect(renderer, &rect) _ = SDL.SetRenderDrawColor(renderer, 95, 120, 165, 255) _ = SDL.RenderRect(renderer, &rect) title := fmt.ctprintf("References (%d)", len(panel.items) if panel.pending_id == 0 else 0) _ = SDL.SetRenderDrawColor(renderer, 145, 165, 205, 255) _ = SDL.RenderDebugText(renderer, x + 10, y + 8, title) item_y := y + 28 first_index := 0 if panel.selected >= 14 { first_index = panel.selected - 13 } for index := first_index; index < len(panel.items) && index < first_index + 14; index += 1 { item := panel.items[index] if index == panel.selected { _ = SDL.SetRenderDrawColor(renderer, 52, 62, 86, 255) row := SDL.FRect{x + 4, item_y - 2, width - 8, 16} _ = SDL.RenderFillRect(renderer, &row) _ = SDL.SetRenderDrawColor(renderer, 255, 210, 120, 255) } else { _ = SDL.SetRenderDrawColor(renderer, 220, 225, 235, 255) } label := fmt.ctprintf("%s", item.label) _ = SDL.RenderDebugText(renderer, x + 10, item_y, label) item_y += 16 } } render_references_panel_gpu :: proc(gpu: ^GPU_Renderer, panel: ^References_Panel) { if !panel.open do return x: f32 = 700 y: f32 = 72 width: f32 = 380 visible := min_int(max_int(len(panel.items), 1), 14) height := f32(30 + visible * 16) gpu_rect(gpu, x, y, width, height, 24, 28, 38, 245) gpu_rect_outline(gpu, x, y, width, height, 95, 120, 165, 255) title := fmt.tprintf("References (%d)", len(panel.items) if panel.pending_id == 0 else 0) gpu_text(gpu, x + 10, y + 8, title, 145, 165, 205, 255) item_y := y + 28 first_index := 0 if panel.selected >= 14 { first_index = panel.selected - 13 } for index := first_index; index < len(panel.items) && index < first_index + 14; index += 1 { item := panel.items[index] if index == panel.selected { gpu_rect(gpu, x + 4, item_y - 2, width - 8, 16, 52, 62, 86, 255) gpu_text_limited(gpu, x + 10, item_y, item.label, 46, 255, 210, 120, 255) } else { gpu_text_limited(gpu, x + 10, item_y, item.label, 46, 220, 225, 235, 255) } item_y += 16 } } render_hover_tooltip :: proc(renderer: ^SDL.Renderer, editor: ^Editor, view: ^SDL_View, hover: ^Hover_Tooltip) { if !hover.open do return if len(hover.contents) == 0 do return active := editor_active_buffer(editor) if active == nil do return cursor_line, cursor_col := buffer_offset_to_line_col(&active.buffer, active.cursor.offset) x := f32(editor_text_x(view) + cursor_col * SDL.DEBUG_TEXT_FONT_CHARACTER_SIZE) y := f32(SDL_EDITOR_TEXT_Y + (cursor_line - view.first_line + 2) * SDL_LINE_HEIGHT) if y > 690 do y = 690 width := f32(max_int(180, min_int(520, len(hover.contents) * SDL.DEBUG_TEXT_FONT_CHARACTER_SIZE + 20))) height: f32 = 34 _ = SDL.SetRenderDrawColor(renderer, 30, 34, 45, 245) rect := SDL.FRect{x, y, width, height} _ = SDL.RenderFillRect(renderer, &rect) _ = SDL.SetRenderDrawColor(renderer, 100, 125, 165, 255) _ = SDL.RenderRect(renderer, &rect) _ = SDL.SetRenderDrawColor(renderer, 220, 225, 235, 255) text := fmt.ctprintf("%s", hover.contents) _ = SDL.RenderDebugText(renderer, x + 10, y + 10, text) } render_hover_tooltip_gpu :: proc(gpu: ^GPU_Renderer, editor: ^Editor, view: ^SDL_View, hover: ^Hover_Tooltip) { if !hover.open || len(hover.contents) == 0 do return active := editor_active_buffer(editor) if active == nil do return cursor_line, cursor_col := buffer_offset_to_line_col(&active.buffer, active.cursor.offset) x := editor_column_pixel_x_gpu(gpu, view, &active.buffer, cursor_line, cursor_col) y := f32(SDL_EDITOR_TEXT_Y + (cursor_line - view.first_line + 2) * SDL_LINE_HEIGHT) if y > f32(gpu.height - 70) do y = f32(gpu.height - 70) width := f32(max_int(180, min_int(520, gpu_font_text_width(gpu, hover.contents) + 20))) gpu_rect(gpu, x, y, width, 34, 30, 34, 45, 245) gpu_rect_outline(gpu, x, y, width, 34, 100, 125, 165, 255) gpu_text_limited(gpu, x + 10, y + 10, hover.contents, 80, 220, 225, 235, 255) } render_completion_popup :: proc(renderer: ^SDL.Renderer, editor: ^Editor, view: ^SDL_View, popup: ^Completion_Popup) { if !popup.open do return active := editor_active_buffer(editor) if active == nil do return cursor_line, cursor_col := buffer_offset_to_line_col(&active.buffer, active.cursor.offset) x := f32(editor_text_x(view) + cursor_col * SDL.DEBUG_TEXT_FONT_CHARACTER_SIZE) y := f32(SDL_EDITOR_TEXT_Y + (cursor_line - view.first_line + 1) * SDL_LINE_HEIGHT) if y > 650 do y = 650 width: f32 = 260 height := f32(26 + min_int(len(popup.items), 8) * 16) _ = SDL.SetRenderDrawColor(renderer, 28, 32, 43, 245) rect := SDL.FRect{x, y, width, height} _ = SDL.RenderFillRect(renderer, &rect) _ = SDL.SetRenderDrawColor(renderer, 90, 110, 150, 255) _ = SDL.RenderRect(renderer, &rect) _ = SDL.SetRenderDrawColor(renderer, 145, 165, 205, 255) _ = SDL.RenderDebugText(renderer, x + 8, y + 6, "Completions") item_y := y + 22 first_index := 0 if popup.selected >= 8 { first_index = popup.selected - 7 } for index := first_index; index < len(popup.items) && index < first_index + 8; index += 1 { item := popup.items[index] if index == popup.selected { _ = SDL.SetRenderDrawColor(renderer, 52, 62, 86, 255) row := SDL.FRect{x + 4, item_y - 2, width - 8, 16} _ = SDL.RenderFillRect(renderer, &row) _ = SDL.SetRenderDrawColor(renderer, 255, 210, 120, 255) } else { _ = SDL.SetRenderDrawColor(renderer, 220, 225, 235, 255) } item_c := fmt.ctprintf("%s", item.label) _ = SDL.RenderDebugText(renderer, x + 10, item_y, item_c) if len(item.kind) > 0 && item.kind != "status" { kind_c := fmt.ctprintf("%s", item.kind) _ = SDL.RenderDebugText(renderer, x + width - 78, item_y, kind_c) } item_y += 16 } } render_completion_popup_gpu :: proc(gpu: ^GPU_Renderer, editor: ^Editor, view: ^SDL_View, popup: ^Completion_Popup) { if !popup.open do return active := editor_active_buffer(editor) if active == nil do return cursor_line, cursor_col := buffer_offset_to_line_col(&active.buffer, active.cursor.offset) x := editor_column_pixel_x_gpu(gpu, view, &active.buffer, cursor_line, cursor_col) y := f32(SDL_EDITOR_TEXT_Y + (cursor_line - view.first_line + 1) * SDL_LINE_HEIGHT) if y > f32(gpu.height - 110) do y = f32(gpu.height - 110) width: f32 = 260 height := f32(26 + min_int(len(popup.items), 8) * 16) gpu_rect(gpu, x, y, width, height, 28, 32, 43, 245) gpu_rect_outline(gpu, x, y, width, height, 90, 110, 150, 255) gpu_text(gpu, x + 8, y + 6, "Completions", 145, 165, 205, 255) item_y := y + 22 first_index := 0 if popup.selected >= 8 { first_index = popup.selected - 7 } for index := first_index; index < len(popup.items) && index < first_index + 8; index += 1 { item := popup.items[index] if index == popup.selected { gpu_rect(gpu, x + 4, item_y - 2, width - 8, 16, 52, 62, 86, 255) gpu_text_limited(gpu, x + 10, item_y, item.label, 24, 255, 210, 120, 255) } else { gpu_text_limited(gpu, x + 10, item_y, item.label, 24, 220, 225, 235, 255) } if len(item.kind) > 0 && item.kind != "status" { gpu_text_limited(gpu, x + width - 78, item_y, item.kind, 10, 140, 146, 158, 255) } item_y += 16 } } render_selection_for_line :: proc(renderer: ^SDL.Renderer, view: ^SDL_View, active: ^Editor_Buffer, line_index: int, y: f32) { selection_start, selection_end, ok := editor_selection_range(active) if !ok do return line_start := buffer_line_start(&active.buffer, line_index) line_end := buffer_line_end(&active.buffer, line_index) if selection_end < line_start || selection_start > line_end do return start := max_int(selection_start, line_start) end := min_int(selection_end, line_end) if start == end { if selection_end != line_start do return end = min_int(line_end, start + 1) } start_col := start - line_start end_col := max_int(end - line_start, start_col + 1) x := f32(editor_text_x(view) + start_col * SDL.DEBUG_TEXT_FONT_CHARACTER_SIZE) width := f32((end_col - start_col) * SDL.DEBUG_TEXT_FONT_CHARACTER_SIZE) rect := SDL.FRect{x, y - 2, width, 16} _ = SDL.SetRenderDrawColor(renderer, 62, 83, 125, 180) _ = SDL.RenderFillRect(renderer, &rect) } render_selection_for_line_gpu :: proc(gpu: ^GPU_Renderer, view: ^SDL_View, active: ^Editor_Buffer, line_index: int, y: f32) { selection_start, selection_end, ok := editor_selection_range(active) if !ok do return line_start := buffer_line_start(&active.buffer, line_index) line_end := buffer_line_end(&active.buffer, line_index) if selection_end < line_start || selection_start > line_end do return start := max_int(selection_start, line_start) end := min_int(selection_end, line_end) if start == end { if selection_end != line_start do return end = min_int(line_end, start + 1) } start_col := start - line_start end_col := max_int(end - line_start, start_col + 1) x := editor_column_pixel_x_gpu(gpu, view, &active.buffer, line_index, start_col) width := round_f32(line_prefix_pixel_advance_gpu(gpu, &active.buffer, line_index, end_col) - line_prefix_pixel_advance_gpu(gpu, &active.buffer, line_index, start_col)) gpu_rect(gpu, x, y - 3, width, SDL_LINE_HEIGHT + 1, 58, 91, 140, 170) } render_search_matches_for_line_gpu :: proc(gpu: ^GPU_Renderer, view: ^SDL_View, active: ^Editor_Buffer, panel: ^Search_Panel, line_index: int, line: string, y: f32) { if !panel.open || len(panel.input) == 0 || len(line) == 0 do return query := panel.input[:] if len(query) > len(line) do return at := 0 for at <= len(line) - len(query) { match := true for i := 0; i < len(query); i += 1 { if line[at + i] != query[i] { match = false break } } if match { x := editor_column_pixel_x_gpu(gpu, view, &active.buffer, line_index, at) width := round_f32(line_prefix_pixel_advance_gpu(gpu, &active.buffer, line_index, at + len(query)) - line_prefix_pixel_advance_gpu(gpu, &active.buffer, line_index, at)) gpu_rect(gpu, x, y - 3, width, SDL_LINE_HEIGHT + 1, 94, 75, 31, 190) gpu_rect_outline(gpu, x, y - 3, width, SDL_LINE_HEIGHT + 1, 181, 137, 45, 220) at += max_int(len(query), 1) } else { at += 1 } } } render_diagnostic_underline_gpu :: proc(gpu: ^GPU_Renderer, view: ^SDL_View, buffer: ^Buffer, line_index, column: int, y: f32) { start_col := max_int(column, 0) end_col := start_col + 6 line_end_col := buffer_line_end(buffer, line_index) - buffer_line_start(buffer, line_index) end_col = clamp_int(end_col, start_col + 1, max_int(line_end_col, start_col + 1)) x0 := editor_column_pixel_x_gpu(gpu, view, buffer, line_index, start_col) x1 := editor_column_pixel_x_gpu(gpu, view, buffer, line_index, end_col) underline_y := y + SDL_LINE_HEIGHT - 2 segment: f32 = 3 x := x0 for x < x1 { gpu_rect(gpu, x, underline_y, min_f32(segment, x1 - x), 1, 244, 113, 116, 255) x += segment * 2 } } render_project_tree :: proc(renderer: ^SDL.Renderer, tree: ^Project_Tree, view: ^SDL_View, active_path: string) { if !view.explorer_visible do return sidebar_width := left_sidebar_width(view) _ = SDL.SetRenderDrawColor(renderer, 20, 23, 31, 255) sidebar := SDL.FRect{0, 0, f32(sidebar_width), f32(view.window_height)} _ = SDL.RenderFillRect(renderer, &sidebar) _ = SDL.SetRenderDrawColor(renderer, 145, 165, 205, 255) _ = SDL.RenderDebugText(renderer, 12, 8, "Project") y: f32 = 28 end := min_int(view.tree_first + visible_tree_rows(view), len(tree.files)) for index := view.tree_first; index < end; index += 1 { item := tree.files[index] if item.path == active_path { _ = SDL.SetRenderDrawColor(renderer, 37, 45, 63, 255) row := SDL.FRect{0, y - 2, f32(sidebar_width), 16} _ = SDL.RenderFillRect(renderer, &row) } label := fmt.ctprintf("%s%s", "/ " if item.is_dir else " ", item.label) if item.is_dir { _ = SDL.SetRenderDrawColor(renderer, 145, 165, 205, 255) } else if item.path == active_path { _ = SDL.SetRenderDrawColor(renderer, 255, 210, 120, 255) } else { _ = SDL.SetRenderDrawColor(renderer, 190, 198, 215, 255) } _ = SDL.RenderDebugText(renderer, 12, y, label) y += 16 } } render_project_tree_gpu :: proc(gpu: ^GPU_Renderer, tree: ^Project_Tree, view: ^SDL_View, active_path: string) { if !view.explorer_visible do return sidebar_width := left_sidebar_width(view) gpu_rect(gpu, 0, SDL_TOP_BAR_HEIGHT, f32(sidebar_width), f32(gpu.height - SDL_TOP_BAR_HEIGHT - SDL_STATUS_BAR_HEIGHT), 35, 36, 40, 255) gpu_rect(gpu, 0, SDL_TOP_BAR_HEIGHT, f32(sidebar_width), 1, 55, 57, 64, 255) gpu_text(gpu, 14, SDL_TREE_HEADER_Y, "EXPLORER", 150, 154, 162, 255) gpu_rect(gpu, 14, SDL_TREE_HEADER_Y + 18, f32(sidebar_width - 28), 1, 52, 54, 60, 255) gpu_rect(gpu, f32(sidebar_width - 2), SDL_TOP_BAR_HEIGHT, 4, f32(gpu.height - SDL_TOP_BAR_HEIGHT - SDL_STATUS_BAR_HEIGHT), 57, 60, 68, 255) y: f32 = SDL_TREE_FIRST_Y end := min_int(view.tree_first + visible_tree_rows(view), len(tree.files)) for index := view.tree_first; index < end; index += 1 { item := tree.files[index] if item.path == active_path { gpu_rect(gpu, 8, y - 3, f32(sidebar_width - 16), SDL_TREE_ROW_HEIGHT + 1, 49, 51, 58, 255) gpu_rect(gpu, 8, y - 3, 2, SDL_TREE_ROW_HEIGHT + 1, 77, 155, 230, 255) } label := fmt.tprintf("%s%s", "/ " if item.is_dir else " ", item.label) max_columns := max_int(int(f32(sidebar_width - 32) / max_f32(gpu.font_advance, 1)), 1) if item.is_dir { gpu_text_limited(gpu, 16, y, label, max_columns, 180, 184, 194, 255) } else if item.path == active_path { gpu_text_limited(gpu, 16, y, label, max_columns, 235, 237, 241, 255) } else { gpu_text_limited(gpu, 16, y, label, max_columns, 190, 193, 201, 255) } y += SDL_TREE_ROW_HEIGHT } } render_editor_tabs_gpu :: proc(gpu: ^GPU_Renderer, editor: ^Editor, view: ^SDL_View) { tab_x := f32(left_sidebar_width(view) + 12) max_x := f32(gpu.width - 12) for &buffer, index in editor.buffers { if tab_x >= max_x do break _, file_name := os.split_path(buffer.path) width := f32(tab_width_for_label(file_name)) if tab_x + width > max_x { width = max_x - tab_x } active := index == editor.active if active { gpu_rect(gpu, tab_x, SDL_TOP_BAR_HEIGHT + 5, width, SDL_TAB_BAR_HEIGHT - 5, 27, 28, 32, 255) gpu_rect(gpu, tab_x, SDL_TOP_BAR_HEIGHT + SDL_TAB_BAR_HEIGHT - 2, width, 2, 77, 155, 230, 255) } else { gpu_rect(gpu, tab_x, SDL_TOP_BAR_HEIGHT + 6, width, SDL_TAB_BAR_HEIGHT - 6, 41, 43, 48, 255) } dirty_marker := " *" if buffer.dirty else "" tab_title := fmt.tprintf("%s%s", file_name, dirty_marker) if active { gpu_text_limited(gpu, tab_x + 16, SDL_TOP_BAR_HEIGHT + 12, tab_title, 34, 220, 223, 228, 255) gpu_text(gpu, tab_x + width - 22, SDL_TOP_BAR_HEIGHT + 12, "x", 168, 172, 182, 255) } else { gpu_text_limited(gpu, tab_x + 16, SDL_TOP_BAR_HEIGHT + 12, tab_title, 34, 168, 172, 182, 255) gpu_text(gpu, tab_x + width - 22, SDL_TOP_BAR_HEIGHT + 12, "x", 124, 128, 138, 255) } gpu_rect(gpu, tab_x + width, SDL_TOP_BAR_HEIGHT + 8, 1, SDL_TAB_BAR_HEIGHT - 12, 56, 58, 64, 255) tab_x += width + 4 } } render_metrics_overlay_gpu :: proc(gpu: ^GPU_Renderer, editor: ^Editor, view: ^SDL_View) { if !view.show_metrics do return active := editor_active_buffer(editor) if active == nil do return line, column := buffer_offset_to_line_col(&active.buffer, active.cursor.offset) prefix_width := line_prefix_pixel_advance_gpu(gpu, &active.buffer, line, column) text := fmt.tprintf("metrics: advance=%.2f line=%d col=%d prefix=%.2f x=%.2f", gpu.font_advance, line + 1, column + 1, prefix_width, editor_column_pixel_x_gpu(gpu, view, &active.buffer, line, column)) width := f32(max_int(gpu_font_text_width(gpu, text) + 20, 420)) x := f32(left_sidebar_width(view) + 16) y := f32(SDL_EDITOR_TOP + 10) gpu_rect(gpu, x, y, width, 34, 22, 24, 29, 235) gpu_rect_outline(gpu, x, y, width, 34, 77, 155, 230, 255) gpu_text(gpu, x + 10, y + 10, text, 220, 230, 245, 255) } render_top_bar_gpu :: proc(gpu: ^GPU_Renderer, active: ^Editor_Buffer) { gpu_rect(gpu, 0, 0, f32(gpu.width), SDL_TOP_BAR_HEIGHT, 43, 45, 50, 255) gpu_rect(gpu, 0, SDL_TOP_BAR_HEIGHT - 1, f32(gpu.width), 1, 56, 58, 64, 255) gpu_text(gpu, 14, 10, "Native Kotlin Editor", 220, 223, 228, 255) line, column := buffer_offset_to_line_col(&active.buffer, active.cursor.offset) right := fmt.tprintf("Ln %d, Col %d", line + 1, column + 1) gpu_text_limited(gpu, f32(gpu.width - 150), 10, right, 20, 172, 176, 186, 255) } render_status_bar_gpu :: proc(gpu: ^GPU_Renderer, message: string, diagnostic: bool) { y := f32(gpu.height - SDL_STATUS_BAR_HEIGHT) if diagnostic { gpu_rect(gpu, 0, y, f32(gpu.width), SDL_STATUS_BAR_HEIGHT, 122, 50, 54, 255) gpu_text_limited(gpu, 14, y + 7, message, 120, 255, 235, 235, 255) } else { gpu_rect(gpu, 0, y, f32(gpu.width), SDL_STATUS_BAR_HEIGHT, 34, 92, 143, 255) gpu_text_limited(gpu, 14, y + 7, message, 120, 232, 242, 255, 255) } gpu_text(gpu, f32(gpu.width - 156), y + 7, "Odin + SDL3 GPU", 218, 232, 246, 255) } gpu_rect_outline :: proc(gpu: ^GPU_Renderer, x, y, w, h: f32, r, g, b, a: u8) { gpu_rect(gpu, x, y, w, 1, r, g, b, a) gpu_rect(gpu, x, y + h - 1, w, 1, r, g, b, a) gpu_rect(gpu, x, y, 1, h, r, g, b, a) gpu_rect(gpu, x + w - 1, y, 1, h, r, g, b, a) }