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>
3847 lines
146 KiB
Odin
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)
|
|
}
|