editor/client/odin/sdl_app.odin
pavel 4a15350860 Initial commit: Odin editor client + Kotlin daemon prototype
Native SDL3 editor written in Odin with a piece-table buffer core,
backed by a Kotlin/JVM daemon speaking newline-delimited JSON over
localhost TCP for Gradle import, Kotlin/Java diagnostics, and
heuristic completion/hover/definition/references/rename.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 08:37:41 +02:00

3847 lines
146 KiB
Odin

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)
}