4980 lines
193 KiB
Odin
4980 lines
193 KiB
Odin
package main
|
|
|
|
import "core:c"
|
|
import "core:fmt"
|
|
import "core:os"
|
|
import "base:runtime"
|
|
import "core:slice"
|
|
import "core:strings"
|
|
import "core:sync"
|
|
import "core:time"
|
|
import json "core:encoding/json"
|
|
import SDL "vendor:sdl3"
|
|
|
|
SDL_View :: struct {
|
|
first_line: int,
|
|
tree_first: int,
|
|
mouse_x: f32,
|
|
mouse_y: f32,
|
|
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,
|
|
left_tab: Left_Tab,
|
|
gradle_sidebar_visible: bool,
|
|
terminal_visible: bool,
|
|
terminal_height: int,
|
|
cached_workspace: string,
|
|
saved_active_file: int,
|
|
saved_open_files: [dynamic]Saved_Open_File,
|
|
}
|
|
|
|
Saved_Open_File :: struct {
|
|
path: string,
|
|
cursor: int,
|
|
}
|
|
|
|
Left_Tab :: enum {
|
|
Files,
|
|
Git,
|
|
}
|
|
|
|
Resize_Panel :: enum {
|
|
None,
|
|
Left_Sidebar,
|
|
Right_Sidebar,
|
|
Bottom_Panel,
|
|
}
|
|
|
|
Project_File :: struct {
|
|
path: string,
|
|
name: string,
|
|
depth: int,
|
|
is_dir: bool,
|
|
expanded: bool,
|
|
}
|
|
|
|
Project_Tree :: struct {
|
|
workspace: string,
|
|
files: [dynamic]Project_File,
|
|
expanded: map[string]bool,
|
|
}
|
|
|
|
// State for the system folder-selection dialog. SDL may invoke the dialog
|
|
// callback on a different thread, so the selection is handed to the main
|
|
// loop under a mutex.
|
|
Folder_Dialog_State :: struct {
|
|
window: ^SDL.Window,
|
|
mutex: sync.Mutex,
|
|
active: bool,
|
|
pending: bool,
|
|
path: [dynamic]u8,
|
|
}
|
|
|
|
Context_Menu :: struct {
|
|
open: bool,
|
|
// Daemon state snapshotted when the menu opens, so rendering and click
|
|
// handling grey out the same items.
|
|
daemon_ok: bool,
|
|
x: f32,
|
|
y: f32,
|
|
}
|
|
|
|
Context_Menu_Action :: enum {
|
|
Cut,
|
|
Copy,
|
|
Paste,
|
|
Definition,
|
|
References,
|
|
Rename,
|
|
}
|
|
|
|
Context_Menu_Item :: struct {
|
|
label: string,
|
|
action: Context_Menu_Action,
|
|
separator_before: bool,
|
|
}
|
|
|
|
@(rodata)
|
|
context_menu_items := [?]Context_Menu_Item{
|
|
{label = "Cut", action = .Cut},
|
|
{label = "Copy", action = .Copy},
|
|
{label = "Paste", action = .Paste},
|
|
{label = "Go to Definition", action = .Definition, separator_before = true},
|
|
{label = "Find References", action = .References},
|
|
{label = "Rename...", action = .Rename},
|
|
}
|
|
|
|
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,
|
|
// Mouse-dwell hovers anchor to the hovered identifier and dismiss when
|
|
// the mouse leaves its column span; keyboard hovers anchor to the cursor.
|
|
from_mouse: bool,
|
|
word_start: int,
|
|
word_end: int,
|
|
}
|
|
|
|
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 {
|
|
Open_Folder,
|
|
Reload_Workspace,
|
|
Save,
|
|
Save_All,
|
|
Close_Tab,
|
|
Find,
|
|
Diagnostics,
|
|
Gradle_Tasks,
|
|
Explorer,
|
|
Metrics,
|
|
Completion,
|
|
Hover,
|
|
Definition,
|
|
References,
|
|
Rename,
|
|
}
|
|
|
|
Command_Item :: struct {
|
|
id: Command_Id,
|
|
label: string,
|
|
}
|
|
|
|
COMMAND_ITEMS := [?]Command_Item{
|
|
{.Open_Folder, "Open Folder"},
|
|
{.Reload_Workspace, "Reload Workspace"},
|
|
{.Save, "Save File"},
|
|
{.Save_All, "Save All"},
|
|
{.Close_Tab, "Close Tab"},
|
|
{.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,
|
|
group: string,
|
|
}
|
|
|
|
Gradle_Tasks_Panel :: struct {
|
|
open: bool,
|
|
pending_id: int,
|
|
pending_run: bool,
|
|
selected: int,
|
|
scroll: int,
|
|
items: [dynamic]Gradle_Task_Item,
|
|
collapsed: map[string]bool,
|
|
message: string,
|
|
output: [dynamic]string,
|
|
}
|
|
|
|
// One visible row of the Gradle tree: project -> group -> task.
|
|
Gradle_Row_Kind :: enum {
|
|
Project,
|
|
Group,
|
|
Task,
|
|
}
|
|
|
|
Gradle_Row :: struct {
|
|
kind: Gradle_Row_Kind,
|
|
label: string,
|
|
key: string, // collapse-state key for project and group rows
|
|
item_index: int,
|
|
}
|
|
|
|
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_TOOL_STRIP_WIDTH :: 26
|
|
SDL_DAEMON_SYNC_DEBOUNCE_MS :: 250
|
|
SDL_HOVER_DWELL_MS :: 450
|
|
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,
|
|
terminal_height = SDL_TERMINAL_DEFAULT_HEIGHT,
|
|
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 visible, has_visible := json_get_bool(value, "terminalVisible"); has_visible {
|
|
view.terminal_visible = visible
|
|
}
|
|
if height, has_height := json_get_int(value, "terminalHeight"); has_height {
|
|
view.terminal_height = clamp_int(height, SDL_TERMINAL_MIN_HEIGHT, 2000)
|
|
}
|
|
if tab, has_tab := json_get_int(value, "leftTab"); has_tab {
|
|
view.left_tab = Left_Tab(clamp_int(tab, 0, len(Left_Tab) - 1))
|
|
}
|
|
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\":%s,\"cursor\":%d}}", json_quote(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 \"terminalVisible\": %v,\n \"terminalHeight\": %d,\n \"leftTab\": %d,\n \"workspace\": %s,\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.terminal_visible, view.terminal_height, int(view.left_tab), json_quote(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
|
|
}
|
|
|
|
// The left tool strip owns the leftmost pixels below the top bar; sidebar
|
|
// content starts after it.
|
|
content_left_edge :: proc() -> int {
|
|
return SDL_TOOL_STRIP_WIDTH
|
|
}
|
|
|
|
left_sidebar_width :: proc(view: ^SDL_View) -> int {
|
|
width := SDL_TOOL_STRIP_WIDTH
|
|
if view.explorer_visible {
|
|
width += clamp_int(view.sidebar_width, SDL_SIDEBAR_MIN_WIDTH, SDL_SIDEBAR_MAX_WIDTH)
|
|
}
|
|
return width
|
|
}
|
|
|
|
// The tool strip owns the rightmost pixels below the top bar, so panels and
|
|
// editor content end at this edge instead of the window edge.
|
|
content_right_edge :: proc(window_width: int) -> int {
|
|
return window_width - SDL_TOOL_STRIP_WIDTH
|
|
}
|
|
|
|
// Effective bottom-panel height: user-resizable, kept within the window.
|
|
bottom_panel_height :: proc(view: ^SDL_View) -> int {
|
|
max_height := max_int(view.window_height - SDL_STATUS_BAR_HEIGHT - SDL_EDITOR_TOP - 120, SDL_TERMINAL_MIN_HEIGHT)
|
|
return clamp_int(view.terminal_height, SDL_TERMINAL_MIN_HEIGHT, max_height)
|
|
}
|
|
|
|
// Bottom edge of the editor area: the bottom panel takes space above the
|
|
// status bar when visible.
|
|
editor_content_bottom :: proc(view: ^SDL_View) -> int {
|
|
bottom := view.window_height - SDL_STATUS_BAR_HEIGHT
|
|
if view.terminal_visible do bottom -= bottom_panel_height(view)
|
|
return bottom
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
first_file, found := workspace_find_first_file(workspace, 0)
|
|
if !found do return false
|
|
defer delete(first_file)
|
|
return editor_open_file(editor, first_file)
|
|
}
|
|
|
|
run_sdl_editor :: proc(initial_workspace: string, daemon_port: int, file_path: string) {
|
|
view := ui_state_load()
|
|
defer ui_state_destroy(&view)
|
|
|
|
// No workspace argument means reopen the last workspace, falling back to
|
|
// the current directory on first launch or if it no longer exists.
|
|
resolved_workspace := initial_workspace
|
|
if len(resolved_workspace) == 0 {
|
|
if len(view.cached_workspace) > 0 && os.is_dir(view.cached_workspace) {
|
|
resolved_workspace = view.cached_workspace
|
|
} else {
|
|
resolved_workspace = "."
|
|
}
|
|
}
|
|
workspace := strings.clone(resolved_workspace)
|
|
defer delete(workspace)
|
|
|
|
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)
|
|
ns_resize_cursor := SDL.CreateSystemCursor(.NS_RESIZE)
|
|
pointer_cursor := SDL.CreateSystemCursor(.POINTER)
|
|
defer {
|
|
if text_cursor != nil do SDL.DestroyCursor(text_cursor)
|
|
if resize_cursor != nil do SDL.DestroyCursor(resize_cursor)
|
|
if ns_resize_cursor != nil do SDL.DestroyCursor(ns_resize_cursor)
|
|
if pointer_cursor != nil do SDL.DestroyCursor(pointer_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)
|
|
context_menu := Context_Menu{}
|
|
terminal := Terminal_Panel{}
|
|
defer terminal_destroy(&terminal)
|
|
git_panel := Git_Panel{}
|
|
defer git_panel_destroy(&git_panel)
|
|
git_status := Git_Status_Panel{}
|
|
defer git_status_destroy(&git_status)
|
|
terminal.open = view.terminal_visible
|
|
if terminal.open {
|
|
_ = terminal_start(&terminal, workspace)
|
|
}
|
|
dialog := Folder_Dialog_State{window = window}
|
|
defer folder_dialog_destroy(&dialog)
|
|
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)
|
|
}
|
|
|
|
mouse_moved_at := time.now()
|
|
mouse_dwell_handled := false
|
|
|
|
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 context_menu.open {
|
|
context_menu.open = false
|
|
if event.key.key == SDL.K_ESCAPE do continue
|
|
}
|
|
if event.key.key == SDL.K_GRAVE && (event.key.mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE {
|
|
terminal_toggle(&terminal, &git_panel, &view, workspace)
|
|
continue
|
|
}
|
|
if handle_git_status_key(&git_status, &view, workspace, event.key.key, event.key.mod) {
|
|
continue
|
|
}
|
|
if terminal_handle_key(&terminal, event.key.key, event.key.mod) {
|
|
continue
|
|
}
|
|
if handle_sdl_key(&editor, &view, &tree, workspace, &dialog,&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 git_status.input_focused && git_status_panel_active(&view) {
|
|
git_status_input_text(&git_status, text)
|
|
continue
|
|
}
|
|
if terminal.open && terminal.focused {
|
|
terminal_send(&terminal, text)
|
|
continue
|
|
}
|
|
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 && active.diff == 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:
|
|
view.mouse_x = event.wheel.mouse_x
|
|
view.mouse_y = event.wheel.mouse_y
|
|
mouse_moved_at = time.now()
|
|
mouse_dwell_handled = false
|
|
if hover.open && hover.from_mouse {
|
|
hover.open = false
|
|
}
|
|
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_terminal_wheel(&terminal, &git_panel, &view, &tasks_panel, event.wheel.mouse_x, event.wheel.mouse_y, int(event.wheel.integer_y)) {
|
|
// Scrolled the terminal history.
|
|
} 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 handle_git_status_wheel(&git_status, &view, event.wheel.mouse_x, event.wheel.mouse_y, int(event.wheel.integer_y)) {
|
|
// Scrolled the git status list.
|
|
} 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_RIGHT {
|
|
if gpu.available {
|
|
_ = handle_editor_right_click(&context_menu, &editor, &view, &gpu, &tasks_panel, &daemon, event.button.x, event.button.y)
|
|
}
|
|
continue
|
|
}
|
|
if event.button.button != SDL.BUTTON_LEFT do continue
|
|
if handle_context_menu_click(&context_menu, &editor, &view, &completion, &hover, &definition, &references, &rename, &daemon, &daemon_sync_state, event.button.x, event.button.y) {
|
|
view.mouse_selecting = false
|
|
} else if handle_panel_resize_down(&view, &tasks_panel, event.button.x, event.button.y) {
|
|
view.mouse_selecting = false
|
|
set_editor_cursor(&view, &editor, &tree, &tasks_panel, &command_palette, &context_menu, &references, &diagnostics_panel, event.button.x, event.button.y, default_cursor, text_cursor, resize_cursor, ns_resize_cursor, pointer_cursor)
|
|
} else if gpu.available && handle_toolbar_click(&dialog, workspace, event.button.x, event.button.y) {
|
|
view.mouse_selecting = false
|
|
} else if gpu.available && handle_bottom_panel_click(&terminal, &git_panel, &view, &tasks_panel, workspace, event.button.x, event.button.y) {
|
|
view.mouse_selecting = false
|
|
} else if gpu.available && handle_tool_strip_click(&view, &tasks_panel, &terminal, &git_panel, workspace, &editor, &daemon, &daemon_sync_state, event.button.x, event.button.y) {
|
|
view.mouse_selecting = false
|
|
} else if gpu.available && handle_left_strip_click(&view, &git_status, workspace, event.button.x, event.button.y) {
|
|
view.mouse_selecting = false
|
|
} else if gpu.available && handle_git_status_click(&git_status, &view, workspace, event.button.x, event.button.y) {
|
|
view.mouse_selecting = false
|
|
} else if handle_command_palette_click(&command_palette, &editor, &view, &tree, workspace, &dialog,&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, &daemon, event.button.x, event.button.y, int(event.button.clicks)) {
|
|
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 {
|
|
terminal.focused = false
|
|
git_status.input_focused = false
|
|
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:
|
|
view.mouse_x = event.motion.x
|
|
view.mouse_y = event.motion.y
|
|
mouse_moved_at = time.now()
|
|
mouse_dwell_handled = false
|
|
if hover.open && hover.from_mouse {
|
|
line, column, over_text := editor_line_col_from_mouse(&editor, &view, &gpu, event.motion.x, event.motion.y)
|
|
if !over_text || line != hover.line || column < hover.word_start || column >= hover.word_end {
|
|
hover.open = false
|
|
}
|
|
}
|
|
if view.resizing_panel == .Bottom_Panel && (event.motion.state & SDL.BUTTON_LMASK) != {} {
|
|
handle_bottom_panel_resize_motion(&view, event.motion.y)
|
|
} else 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, &editor, &tree, &tasks_panel, &command_palette, &context_menu, &references, &diagnostics_panel, event.motion.x, event.motion.y, default_cursor, text_cursor, resize_cursor, ns_resize_cursor, pointer_cursor)
|
|
}
|
|
}
|
|
|
|
refresh_view_size(window, &view)
|
|
|
|
if gpu.available && daemon.connected && !mouse_dwell_handled && !hover.open &&
|
|
!completion.open && !context_menu.open && !command_palette.open &&
|
|
!references.open && !rename.open && !search.open &&
|
|
time.diff(mouse_moved_at, time.now()) >= SDL_HOVER_DWELL_MS * time.Millisecond &&
|
|
editor_text_hit(&view, &tasks_panel, view.mouse_x, view.mouse_y) {
|
|
mouse_dwell_handled = true
|
|
if daemon_sync_state.pending {
|
|
daemon_sync_now(&daemon_sync_state, &daemon, &editor, false)
|
|
}
|
|
_ = hover_tooltip_open_at_mouse(&hover, &editor, &view, &gpu, &daemon, view.mouse_x, view.mouse_y)
|
|
}
|
|
|
|
if selected, has_selected := folder_dialog_take(&dialog); has_selected {
|
|
attempt_open_folder(&editor, &view, &tree, &navigation, &workspace, selected, &daemon, &daemon_sync_state, &owned_daemon, &daemon_port_line, &daemon_start_pending, owned_daemon_enabled)
|
|
delete(selected)
|
|
}
|
|
|
|
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)
|
|
terminal_pump(&terminal)
|
|
git_panel_pump(&git_panel)
|
|
git_status_pump(&git_status)
|
|
if git_status_panel_active(&view) && git_status.loaded_root != workspace {
|
|
git_status_request(&git_status, workspace)
|
|
}
|
|
if git_status.committed {
|
|
// A new commit exists: force the history panel to reload when
|
|
// (or while) it is visible.
|
|
git_status.committed = false
|
|
delete(git_panel.loaded_root)
|
|
git_panel.loaded_root = strings.clone("")
|
|
}
|
|
if git_panel.diff_ready {
|
|
git_panel.diff_ready = false
|
|
editor_open_diff(&editor, git_panel_diff_tab_name(&git_panel), git_panel.diff_text)
|
|
}
|
|
if terminal.open && terminal.active_tab == .Git {
|
|
git_panel_activate(&git_panel, workspace)
|
|
}
|
|
if gpu.available {
|
|
render_sdl_editor_gpu(&gpu, &editor, &view, &tree, &completion, &hover, &references, &rename, &search, &diagnostics_panel, &command_palette, &context_menu, &tasks_panel, &terminal, &git_panel, &git_status)
|
|
} 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.from_mouse = false
|
|
hover.line = line
|
|
hover.column = column
|
|
hover.pending_id = daemon_send_hover(daemon, active.path, line, column)
|
|
hover_tooltip_set_contents(hover, "Loading hover...")
|
|
}
|
|
|
|
buffer_word_span_at :: proc(buffer: ^Buffer, line, column: int) -> (start_col, end_col: int, ok: bool) {
|
|
bytes := buffer_line_bytes(buffer, line)
|
|
defer delete(bytes)
|
|
if column < 0 || column >= len(bytes) do return 0, 0, false
|
|
if !is_identifier_byte(bytes[column]) do return 0, 0, false
|
|
start := column
|
|
for start > 0 && is_identifier_byte(bytes[start - 1]) {
|
|
start -= 1
|
|
}
|
|
end := column
|
|
for end < len(bytes) && is_identifier_byte(bytes[end]) {
|
|
end += 1
|
|
}
|
|
return start, end, true
|
|
}
|
|
|
|
// Opens a hover request for the identifier under the mouse; returns false
|
|
// when the mouse is not over an identifier.
|
|
hover_tooltip_open_at_mouse :: proc(hover: ^Hover_Tooltip, editor: ^Editor, view: ^SDL_View, gpu: ^GPU_Renderer, daemon: ^Daemon_Client, x, y: f32) -> bool {
|
|
active := editor_active_buffer(editor)
|
|
if active == nil || active.scratch do return false
|
|
|
|
line, column, ok := editor_line_col_from_mouse(editor, view, gpu, x, y)
|
|
if !ok do return false
|
|
word_start, word_end, has_word := buffer_word_span_at(&active.buffer, line, column)
|
|
if !has_word do return false
|
|
|
|
hover.open = true
|
|
hover.from_mouse = true
|
|
hover.line = line
|
|
hover.column = column
|
|
hover.word_start = word_start
|
|
hover.word_end = word_end
|
|
hover.pending_id = daemon_send_hover(daemon, active.path, line, column)
|
|
// No "loading" placeholder for dwell hovers: the tooltip stays invisible
|
|
// until real contents arrive, so hovering plain code shows nothing.
|
|
hover_tooltip_set_contents(hover, "")
|
|
return true
|
|
}
|
|
|
|
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, dialog: ^Folder_Dialog_State,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 .Open_Folder:
|
|
folder_dialog_show(dialog, workspace)
|
|
case .Close_Tab:
|
|
if editor_close_active(editor) {
|
|
scroll_sdl_view(editor, view, 0)
|
|
}
|
|
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:
|
|
gradle_panel_toggle(view, tasks_panel, editor, daemon, sync)
|
|
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_panel_toggle :: proc(view: ^SDL_View, tasks_panel: ^Gradle_Tasks_Panel, editor: ^Editor, daemon: ^Daemon_Client, sync: ^Daemon_Sync_State) {
|
|
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)
|
|
}
|
|
|
|
gradle_tasks_panel_destroy :: proc(panel: ^Gradle_Tasks_Panel) {
|
|
gradle_tasks_panel_clear(panel)
|
|
delete(panel.items)
|
|
delete(panel.collapsed)
|
|
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)
|
|
delete(item.group)
|
|
}
|
|
clear(&panel.items)
|
|
for key in panel.collapsed {
|
|
delete(key)
|
|
}
|
|
clear(&panel.collapsed)
|
|
panel.scroll = 0
|
|
}
|
|
|
|
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 :: proc(panel: ^Gradle_Tasks_Panel, daemon: ^Daemon_Client, index: int) {
|
|
if index < 0 || index >= len(panel.items) || panel.pending_id != 0 do return
|
|
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")
|
|
group, _ := json_get_string(task, "group")
|
|
append(&items, Gradle_Task_Item{path = strings.clone(path), name = strings.clone(name), description = strings.clone(description), group = strings.clone(group)})
|
|
}
|
|
}
|
|
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
|
|
}
|
|
|
|
folder_dialog_destroy :: proc(dialog: ^Folder_Dialog_State) {
|
|
delete(dialog.path)
|
|
}
|
|
|
|
folder_dialog_show :: proc(dialog: ^Folder_Dialog_State, workspace: string) {
|
|
if sync.mutex_guard(&dialog.mutex) {
|
|
if dialog.active do return
|
|
dialog.active = true
|
|
}
|
|
location: cstring
|
|
if absolute, err := os.get_absolute_path(workspace, context.temp_allocator); err == nil {
|
|
location = strings.clone_to_cstring(absolute, context.temp_allocator)
|
|
}
|
|
SDL.ShowOpenFolderDialog(folder_dialog_callback, rawptr(dialog), dialog.window, location, false)
|
|
}
|
|
|
|
folder_dialog_callback :: proc "c" (userdata: rawptr, filelist: [^]cstring, filter: c.int) {
|
|
context = runtime.default_context()
|
|
dialog := (^Folder_Dialog_State)(userdata)
|
|
if sync.mutex_guard(&dialog.mutex) {
|
|
dialog.active = false
|
|
dialog.pending = false
|
|
clear(&dialog.path)
|
|
// A nil list is a dialog error; an empty list means the user canceled.
|
|
if filelist != nil && filelist[0] != nil {
|
|
for b in transmute([]u8)string(filelist[0]) {
|
|
append(&dialog.path, b)
|
|
}
|
|
dialog.pending = true
|
|
}
|
|
}
|
|
}
|
|
|
|
folder_dialog_take :: proc(dialog: ^Folder_Dialog_State) -> (string, bool) {
|
|
if sync.mutex_guard(&dialog.mutex) {
|
|
if dialog.pending {
|
|
dialog.pending = false
|
|
return strings.clone(string(dialog.path[:])), true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
workspace_find_first_file :: proc(dir: string, depth: int) -> (string, bool) {
|
|
if depth > 6 do return "", false
|
|
|
|
entries, err := os.read_all_directory_by_path(dir, context.allocator)
|
|
if err != nil do return "", false
|
|
defer os.file_info_slice_delete(entries, context.allocator)
|
|
|
|
slice.sort_by(entries, proc(a, b: os.File_Info) -> bool {
|
|
a_dir := a.type == .Directory
|
|
b_dir := b.type == .Directory
|
|
if a_dir != b_dir do return !a_dir
|
|
return a.name < b.name
|
|
})
|
|
|
|
for entry in entries {
|
|
if project_tree_skip(entry.name) do continue
|
|
if entry.type != .Directory {
|
|
return strings.clone(fmt.tprintf("%s/%s", dir, entry.name)), true
|
|
}
|
|
}
|
|
for entry in entries {
|
|
if project_tree_skip(entry.name) do continue
|
|
if entry.type == .Directory {
|
|
path, ok := workspace_find_first_file(fmt.tprintf("%s/%s", dir, entry.name), depth + 1)
|
|
if ok do return path, true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
attempt_open_folder :: proc(
|
|
editor: ^Editor,
|
|
view: ^SDL_View,
|
|
tree: ^Project_Tree,
|
|
navigation: ^Navigation_History,
|
|
workspace: ^string,
|
|
selected: string,
|
|
daemon: ^Daemon_Client,
|
|
sync: ^Daemon_Sync_State,
|
|
owned_daemon: ^Daemon_Process,
|
|
port_line: ^[dynamic]u8,
|
|
start_pending: ^bool,
|
|
owned_enabled: bool,
|
|
) {
|
|
resolved := strings.trim_space(selected)
|
|
if len(resolved) == 0 || !os.is_dir(resolved) {
|
|
editor_set_status(editor, "Not a directory")
|
|
return
|
|
}
|
|
for &buffer in editor.buffers {
|
|
if buffer.dirty {
|
|
editor_set_status(editor, "Save all buffers before switching folders")
|
|
return
|
|
}
|
|
}
|
|
first_file, found := workspace_find_first_file(resolved, 0)
|
|
if !found {
|
|
editor_set_status(editor, "Folder has no openable files")
|
|
return
|
|
}
|
|
defer delete(first_file)
|
|
|
|
// Persist the old workspace's session before tearing it down.
|
|
ui_state_capture_editor(view, editor, workspace^)
|
|
ui_state_save(view)
|
|
|
|
// Open the new buffer first so the editor never ends up with zero buffers.
|
|
old_count := len(editor.buffers)
|
|
if !editor_open_file(editor, first_file) {
|
|
editor_set_status(editor, "Failed to open a file in that folder")
|
|
return
|
|
}
|
|
for _ in 0 ..< old_count {
|
|
editor_buffer_destroy(&editor.buffers[0])
|
|
ordered_remove(&editor.buffers, 0)
|
|
}
|
|
editor.active = 0
|
|
view.first_line = 0
|
|
|
|
navigation_clear_stack(&navigation.back)
|
|
navigation_clear_stack(&navigation.forward)
|
|
|
|
delete(workspace^)
|
|
workspace^ = strings.clone(resolved)
|
|
project_tree_destroy(tree)
|
|
tree^ = project_tree_load(workspace^)
|
|
view.tree_first = 0
|
|
|
|
// The daemon runs independently of the workspace, so an open connection
|
|
// just needs the new root; only start a daemon if none is running.
|
|
if daemon.connected {
|
|
daemon_send_workspace_open(daemon, workspace^)
|
|
daemon_sync_now(sync, daemon, editor, true)
|
|
} else if owned_enabled {
|
|
_ = daemon_begin_owned_start(workspace^, owned_daemon, port_line, editor, start_pending)
|
|
}
|
|
|
|
editor_set_status(editor, fmt.tprintf("Opened folder: %s", workspace^))
|
|
}
|
|
|
|
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 {
|
|
if hover.from_mouse {
|
|
hover.open = false
|
|
} else {
|
|
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{workspace = strings.clone(workspace)}
|
|
project_tree_rebuild(&tree)
|
|
return tree
|
|
}
|
|
|
|
project_tree_clear_files :: proc(tree: ^Project_Tree) {
|
|
for file in tree.files {
|
|
delete(file.path)
|
|
delete(file.name)
|
|
}
|
|
clear(&tree.files)
|
|
}
|
|
|
|
project_tree_rebuild :: proc(tree: ^Project_Tree) {
|
|
project_tree_clear_files(tree)
|
|
project_tree_append_dir(tree, tree.workspace, 0)
|
|
}
|
|
|
|
project_tree_destroy :: proc(tree: ^Project_Tree) {
|
|
project_tree_clear_files(tree)
|
|
delete(tree.files)
|
|
tree.files = nil
|
|
for key, _ in tree.expanded {
|
|
delete(key)
|
|
}
|
|
delete(tree.expanded)
|
|
tree.expanded = nil
|
|
delete(tree.workspace)
|
|
tree.workspace = ""
|
|
}
|
|
|
|
project_tree_toggle_dir :: proc(tree: ^Project_Tree, index: int) {
|
|
if index < 0 || index >= len(tree.files) do return
|
|
item := tree.files[index]
|
|
if !item.is_dir do return
|
|
|
|
if _, found := tree.expanded[item.path]; found {
|
|
tree.expanded[item.path] = !item.expanded
|
|
} else {
|
|
tree.expanded[strings.clone(item.path)] = true
|
|
}
|
|
project_tree_rebuild(tree)
|
|
}
|
|
|
|
workspace_reload :: proc(editor: ^Editor, view: ^SDL_View, tree: ^Project_Tree, workspace: string, daemon: ^Daemon_Client, sync: ^Daemon_Sync_State) {
|
|
project_tree_rebuild(tree)
|
|
scroll_project_tree(tree, view, 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, depth: int) {
|
|
if depth > 12 || len(tree.files) >= 5000 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)
|
|
|
|
slice.sort_by(entries, proc(a, b: os.File_Info) -> bool {
|
|
a_dir := a.type == .Directory
|
|
b_dir := b.type == .Directory
|
|
if a_dir != b_dir do return a_dir
|
|
return a.name < b.name
|
|
})
|
|
|
|
for entry in entries {
|
|
if len(tree.files) >= 5000 do return
|
|
if project_tree_skip(entry.name) do continue
|
|
|
|
path := fmt.tprintf("%s/%s", dir, entry.name)
|
|
is_dir := entry.type == .Directory
|
|
expanded := is_dir && tree.expanded[path]
|
|
append(&tree.files, Project_File{
|
|
path = strings.clone(path),
|
|
name = strings.clone(entry.name),
|
|
depth = depth,
|
|
is_dir = is_dir,
|
|
expanded = expanded,
|
|
})
|
|
|
|
if expanded {
|
|
child_path := tree.files[len(tree.files) - 1].path
|
|
project_tree_append_dir(tree, child_path, 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, editor: ^Editor, tree: ^Project_Tree, tasks_panel: ^Gradle_Tasks_Panel, command_palette: ^Command_Palette, context_menu: ^Context_Menu, references: ^References_Panel, diagnostics_panel: ^Diagnostics_Panel, x, y: f32, default_cursor, text_cursor, resize_cursor, ns_resize_cursor, pointer_cursor: ^SDL.Cursor) {
|
|
if view.resizing_panel == .Bottom_Panel || (view.resizing_panel == .None && bottom_panel_resize_hit(view, tasks_panel, x, y)) {
|
|
if ns_resize_cursor != nil do _ = SDL.SetCursor(ns_resize_cursor)
|
|
return
|
|
}
|
|
if view.resizing_panel != .None || panel_resize_hit(view, tasks_panel, x, y) {
|
|
if resize_cursor != nil do _ = SDL.SetCursor(resize_cursor)
|
|
return
|
|
}
|
|
if ui_pointer_hit(view, editor, tree, tasks_panel, command_palette, context_menu, references, diagnostics_panel, x, y) {
|
|
if pointer_cursor != nil do _ = SDL.SetCursor(pointer_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)
|
|
}
|
|
|
|
// True when the mouse is over something that reacts to a click: tree rows,
|
|
// editor tabs, overlay list rows, or Gradle task rows.
|
|
ui_pointer_hit :: proc(view: ^SDL_View, editor: ^Editor, tree: ^Project_Tree, tasks_panel: ^Gradle_Tasks_Panel, command_palette: ^Command_Palette, context_menu: ^Context_Menu, references: ^References_Panel, diagnostics_panel: ^Diagnostics_Panel, x, y: f32) -> bool {
|
|
if toolbar_open_folder_hit(x, y) {
|
|
return true
|
|
}
|
|
if context_menu.open {
|
|
if index, on_item := context_menu_item_at(context_menu, x, y); on_item {
|
|
return context_menu_item_enabled(context_menu, editor, context_menu_items[index].action)
|
|
}
|
|
}
|
|
if tool_strip_gradle_hit(view.window_width, x, y) || tool_strip_terminal_hit(view.window_width, x, y) {
|
|
return true
|
|
}
|
|
if left_strip_hit(.Files, x, y) || left_strip_hit(.Git, x, y) {
|
|
return true
|
|
}
|
|
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 + 60 && y < palette_y + height {
|
|
return true
|
|
}
|
|
}
|
|
if references.open {
|
|
visible := min_int(max_int(len(references.items), 1), 14)
|
|
height := f32(30 + visible * 16)
|
|
if x >= 700 && x < 700 + 380 && y >= 100 && y < 72 + height {
|
|
return true
|
|
}
|
|
}
|
|
if diagnostics_panel.open {
|
|
active := editor_active_buffer(editor)
|
|
if active != nil && len(active.diagnostics) > 0 {
|
|
height: f32 = 168
|
|
sidebar_width := left_sidebar_width(view)
|
|
panel_y := f32(editor_content_bottom(view)) - height
|
|
if x >= f32(sidebar_width) && y >= panel_y + 34 && y < panel_y + height {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
if _, _, _, tab_ok := editor_tab_hit(editor, view, x, y); tab_ok {
|
|
return true
|
|
}
|
|
if _, tree_ok := project_tree_row_at(tree, view, x, y); tree_ok {
|
|
return true
|
|
}
|
|
if tasks_panel.open && len(tasks_panel.items) > 0 {
|
|
rows := gradle_panel_rows(tasks_panel)
|
|
if _, on_row := gradle_panel_row_at(tasks_panel, view, rows, x, y); on_row {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
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(content_right_edge(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
|
|
}
|
|
|
|
// The bottom panel resizes vertically by dragging its top edge.
|
|
bottom_panel_resize_hit :: proc(view: ^SDL_View, tasks_panel: ^Gradle_Tasks_Panel, x, y: f32) -> bool {
|
|
if !view.terminal_visible do return false
|
|
right := f32(content_right_edge(view.window_width))
|
|
if tasks_panel != nil && tasks_panel.open {
|
|
right -= f32(right_sidebar_width_view(view, view.window_width))
|
|
}
|
|
top := f32(view.window_height - SDL_STATUS_BAR_HEIGHT - bottom_panel_height(view))
|
|
return x >= f32(left_sidebar_width(view)) && x < right && abs_f32(y - top) <= SDL_RESIZE_HANDLE_WIDTH
|
|
}
|
|
|
|
handle_panel_resize_down :: proc(view: ^SDL_View, tasks_panel: ^Gradle_Tasks_Panel, x, y: f32) -> bool {
|
|
if bottom_panel_resize_hit(view, tasks_panel, x, y) {
|
|
view.resizing_panel = .Bottom_Panel
|
|
return true
|
|
}
|
|
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(content_right_edge(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(editor_content_bottom(view)) do return false
|
|
if x < f32(editor_text_x(view)) do return false
|
|
|
|
right_limit := f32(content_right_edge(view.window_width) - 8)
|
|
if tasks_panel != nil && tasks_panel.open {
|
|
right_limit = f32(content_right_edge(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) - content_left_edge(), 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(content_right_edge(view.window_width) - int(x), SDL_RIGHT_SIDEBAR_MIN_WIDTH, max_width)
|
|
}
|
|
}
|
|
|
|
handle_bottom_panel_resize_motion :: proc(view: ^SDL_View, y: f32) {
|
|
max_height := max_int(view.window_height - SDL_STATUS_BAR_HEIGHT - SDL_EDITOR_TOP - 120, SDL_TERMINAL_MIN_HEIGHT)
|
|
view.terminal_height = clamp_int(view.window_height - SDL_STATUS_BAR_HEIGHT - int(y), SDL_TERMINAL_MIN_HEIGHT, max_height)
|
|
}
|
|
|
|
project_tree_row_at :: proc(tree: ^Project_Tree, view: ^SDL_View, x, y: f32) -> (int, bool) {
|
|
if !view.explorer_visible || view.left_tab != .Files do return 0, false
|
|
if x < f32(content_left_edge()) || x >= f32(left_sidebar_width(view)) do return 0, false
|
|
if y < SDL_TREE_FIRST_Y do return 0, false
|
|
index := view.tree_first + int((y - SDL_TREE_FIRST_Y) / SDL_TREE_ROW_HEIGHT)
|
|
if index < 0 || index >= len(tree.files) do return 0, false
|
|
return index, true
|
|
}
|
|
|
|
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, ok := project_tree_row_at(tree, view, x, y)
|
|
if !ok do return false
|
|
|
|
item := tree.files[index]
|
|
if item.is_dir {
|
|
project_tree_toggle_dir(tree, index)
|
|
scroll_project_tree(tree, view, 0)
|
|
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, dialog: ^Folder_Dialog_State,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, dialog, 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(editor_content_bottom(view)) - 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
|
|
}
|
|
|
|
// Task-list layout shared by rendering and click handling: rows are 18px,
|
|
// or 30px when the task has a description line, and the list area sits
|
|
// between the panel header (92px) and the output section.
|
|
gradle_panel_output_height :: proc(panel: ^Gradle_Tasks_Panel, panel_height: f32) -> f32 {
|
|
if len(panel.output) == 0 do return 0
|
|
return min_f32(160, max_f32(72, panel_height * 0.32))
|
|
}
|
|
|
|
// The task list is an IntelliJ-style tree: one collapsible node per Gradle
|
|
// project, with that project's tasks as children.
|
|
SDL_GRADLE_ROW_HEIGHT :: 17
|
|
SDL_GRADLE_LIST_TOP :: 52
|
|
|
|
gradle_task_project :: proc(path: string) -> string {
|
|
last := strings.last_index_byte(path, ':')
|
|
if last <= 0 do return ":"
|
|
return path[:last]
|
|
}
|
|
|
|
gradle_task_group :: proc(item: Gradle_Task_Item) -> string {
|
|
if len(item.group) == 0 do return "other"
|
|
return item.group
|
|
}
|
|
|
|
// Looks up a node's collapse state, seeding the IntelliJ-like default on
|
|
// first sight: projects start expanded, groups start collapsed.
|
|
gradle_panel_node_collapsed :: proc(panel: ^Gradle_Tasks_Panel, key: string, default_collapsed: bool) -> bool {
|
|
if key not_in panel.collapsed {
|
|
panel.collapsed[strings.clone(key)] = default_collapsed
|
|
}
|
|
return panel.collapsed[key]
|
|
}
|
|
|
|
append_unique_string :: proc(list: ^[dynamic]string, value: string) {
|
|
for existing in list {
|
|
if existing == value do return
|
|
}
|
|
append(list, value)
|
|
}
|
|
|
|
// Flattens the visible tree rows. Slices point into panel.items strings and
|
|
// the result is temp-allocated, so use it within the same frame only.
|
|
gradle_panel_rows :: proc(panel: ^Gradle_Tasks_Panel) -> []Gradle_Row {
|
|
rows := make([dynamic]Gradle_Row, context.temp_allocator)
|
|
projects := make([dynamic]string, context.temp_allocator)
|
|
for item in panel.items {
|
|
append_unique_string(&projects, gradle_task_project(item.path))
|
|
}
|
|
slice.sort(projects[:])
|
|
|
|
for project in projects {
|
|
append(&rows, Gradle_Row{kind = .Project, label = project, key = project})
|
|
if gradle_panel_node_collapsed(panel, project, false) do continue
|
|
|
|
groups := make([dynamic]string, context.temp_allocator)
|
|
for item in panel.items {
|
|
if gradle_task_project(item.path) != project do continue
|
|
append_unique_string(&groups, gradle_task_group(item))
|
|
}
|
|
slice.sort(groups[:])
|
|
|
|
for group in groups {
|
|
group_key := fmt.tprintf("%s|%s", project, group)
|
|
append(&rows, Gradle_Row{kind = .Group, label = group, key = group_key})
|
|
if gradle_panel_node_collapsed(panel, group_key, true) do continue
|
|
for item, index in panel.items {
|
|
if gradle_task_project(item.path) != project || gradle_task_group(item) != group do continue
|
|
label := item.name
|
|
if len(label) == 0 do label = item.path
|
|
append(&rows, Gradle_Row{kind = .Task, label = label, item_index = index})
|
|
}
|
|
}
|
|
}
|
|
return rows[:]
|
|
}
|
|
|
|
gradle_panel_toggle_node :: proc(panel: ^Gradle_Tasks_Panel, key: string) {
|
|
// Rows are built before any click lands on them, so the key is present.
|
|
if key in panel.collapsed {
|
|
panel.collapsed[key] = !panel.collapsed[key]
|
|
}
|
|
}
|
|
|
|
gradle_panel_geometry :: proc(view: ^SDL_View, window_width, window_height: int) -> (x, y, width, height: f32) {
|
|
width = f32(right_sidebar_width_view(view, window_width))
|
|
height = f32(window_height - SDL_TOP_BAR_HEIGHT - SDL_STATUS_BAR_HEIGHT)
|
|
x = f32(content_right_edge(window_width)) - width
|
|
y = SDL_TOP_BAR_HEIGHT
|
|
return
|
|
}
|
|
|
|
gradle_panel_visible_rows :: proc(panel: ^Gradle_Tasks_Panel, panel_height: f32) -> int {
|
|
list_height := panel_height - SDL_GRADLE_LIST_TOP - gradle_panel_output_height(panel, panel_height) - 6
|
|
return max_int(int(list_height / SDL_GRADLE_ROW_HEIGHT), 1)
|
|
}
|
|
|
|
// Maps a point to a row of the flattened tree; rows must come from
|
|
// gradle_panel_rows this frame.
|
|
gradle_panel_row_at :: proc(panel: ^Gradle_Tasks_Panel, view: ^SDL_View, rows: []Gradle_Row, x, y: f32) -> (int, bool) {
|
|
panel_x, panel_y, width, height := gradle_panel_geometry(view, view.window_width, view.window_height)
|
|
if x < panel_x || x >= panel_x + width do return 0, false
|
|
list_top := panel_y + SDL_GRADLE_LIST_TOP
|
|
if y < list_top do return 0, false
|
|
visible := gradle_panel_visible_rows(panel, height)
|
|
row := int((y - list_top) / SDL_GRADLE_ROW_HEIGHT)
|
|
if row >= visible do return 0, false
|
|
index := panel.scroll + row
|
|
if index < 0 || index >= len(rows) do return 0, false
|
|
return index, true
|
|
}
|
|
|
|
handle_gradle_tasks_click :: proc(view: ^SDL_View, panel: ^Gradle_Tasks_Panel, daemon: ^Daemon_Client, x, y: f32, clicks: int) -> bool {
|
|
if !panel.open do return false
|
|
|
|
panel_x, panel_y, width, height := gradle_panel_geometry(view, view.window_width, view.window_height)
|
|
if x < panel_x || x >= panel_x + width || y < panel_y || y >= panel_y + height do return false
|
|
|
|
rows := gradle_panel_rows(panel)
|
|
index, on_row := gradle_panel_row_at(panel, view, rows, x, y)
|
|
if !on_row do return true
|
|
|
|
row := rows[index]
|
|
if row.kind == .Task {
|
|
panel.selected = row.item_index
|
|
if clicks >= 2 {
|
|
gradle_tasks_panel_run(panel, daemon, row.item_index)
|
|
}
|
|
} else {
|
|
gradle_panel_toggle_node(panel, row.key)
|
|
}
|
|
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
|
|
panel_x, panel_y, width, height := gradle_panel_geometry(view, view.window_width, view.window_height)
|
|
if x < panel_x || x >= panel_x + width || y < panel_y || y >= panel_y + height do return false
|
|
|
|
rows := gradle_panel_rows(panel)
|
|
max_scroll := max_int(len(rows) - gradle_panel_visible_rows(panel, height), 0)
|
|
panel.scroll = clamp_int(panel.scroll - wheel_y * 3, 0, max_scroll)
|
|
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(editor_content_bottom(view)) - 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 || active.diff != 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
|
|
}
|
|
|
|
SDL_CONTEXT_MENU_WIDTH :: 190
|
|
SDL_CONTEXT_MENU_ROW_HEIGHT :: 22
|
|
SDL_CONTEXT_MENU_PADDING :: 6
|
|
SDL_CONTEXT_MENU_SEPARATOR :: 7
|
|
|
|
context_menu_height :: proc() -> f32 {
|
|
height := f32(2 * SDL_CONTEXT_MENU_PADDING)
|
|
for item in context_menu_items {
|
|
if item.separator_before do height += SDL_CONTEXT_MENU_SEPARATOR
|
|
height += SDL_CONTEXT_MENU_ROW_HEIGHT
|
|
}
|
|
return height
|
|
}
|
|
|
|
context_menu_open_at :: proc(menu: ^Context_Menu, view: ^SDL_View, daemon: ^Daemon_Client, x, y: f32) {
|
|
menu.x = max_f32(min_f32(x, f32(view.window_width) - SDL_CONTEXT_MENU_WIDTH - 4), 0)
|
|
menu.y = max_f32(min_f32(y, f32(editor_content_bottom(view)) - context_menu_height() - 4), SDL_TOP_BAR_HEIGHT)
|
|
menu.daemon_ok = daemon.connected
|
|
menu.open = true
|
|
}
|
|
|
|
context_menu_hit :: proc(menu: ^Context_Menu, x, y: f32) -> bool {
|
|
return x >= menu.x && x < menu.x + SDL_CONTEXT_MENU_WIDTH && y >= menu.y && y < menu.y + context_menu_height()
|
|
}
|
|
|
|
context_menu_item_at :: proc(menu: ^Context_Menu, x, y: f32) -> (int, bool) {
|
|
if x < menu.x || x >= menu.x + SDL_CONTEXT_MENU_WIDTH do return 0, false
|
|
row_y := menu.y + SDL_CONTEXT_MENU_PADDING
|
|
for item, index in context_menu_items {
|
|
if item.separator_before do row_y += SDL_CONTEXT_MENU_SEPARATOR
|
|
if y >= row_y && y < row_y + SDL_CONTEXT_MENU_ROW_HEIGHT do return index, true
|
|
row_y += SDL_CONTEXT_MENU_ROW_HEIGHT
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
context_menu_item_enabled :: proc(menu: ^Context_Menu, editor: ^Editor, action: Context_Menu_Action) -> bool {
|
|
active := editor_active_buffer(editor)
|
|
if active == nil do return false
|
|
switch action {
|
|
case .Cut, .Copy:
|
|
_, _, has_selection := editor_selection_range(active)
|
|
return has_selection
|
|
case .Paste:
|
|
return bool(SDL.HasClipboardText())
|
|
case .Definition, .References, .Rename:
|
|
return menu.daemon_ok
|
|
}
|
|
return false
|
|
}
|
|
|
|
context_menu_perform :: proc(action: Context_Menu_Action, editor: ^Editor, view: ^SDL_View, completion: ^Completion_Popup, hover: ^Hover_Tooltip, definition: ^Definition_Jump, references: ^References_Panel, rename: ^Rename_Panel, daemon: ^Daemon_Client, sync: ^Daemon_Sync_State) {
|
|
active := editor_active_buffer(editor)
|
|
if active == nil do return
|
|
|
|
switch action {
|
|
case .Cut:
|
|
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)
|
|
daemon_sync_schedule(sync)
|
|
}
|
|
case .Copy:
|
|
_ = editor_copy_selection_to_clipboard(active)
|
|
case .Paste:
|
|
if editor_paste_clipboard(active) {
|
|
close_stale_edit_overlays(completion, hover, references, rename)
|
|
ensure_cursor_visible(editor, view)
|
|
daemon_sync_schedule(sync)
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
|
|
handle_context_menu_click :: proc(menu: ^Context_Menu, editor: ^Editor, view: ^SDL_View, completion: ^Completion_Popup, hover: ^Hover_Tooltip, definition: ^Definition_Jump, references: ^References_Panel, rename: ^Rename_Panel, daemon: ^Daemon_Client, sync: ^Daemon_Sync_State, x, y: f32) -> bool {
|
|
if !menu.open do return false
|
|
if !context_menu_hit(menu, x, y) {
|
|
menu.open = false
|
|
return true
|
|
}
|
|
index, on_item := context_menu_item_at(menu, x, y)
|
|
if !on_item do return true
|
|
item := context_menu_items[index]
|
|
if !context_menu_item_enabled(menu, editor, item.action) do return true
|
|
menu.open = false
|
|
context_menu_perform(item.action, editor, view, completion, hover, definition, references, rename, daemon, sync)
|
|
return true
|
|
}
|
|
|
|
editor_line_col_from_mouse :: proc(editor: ^Editor, view: ^SDL_View, gpu: ^GPU_Renderer, x, y: f32) -> (int, int, bool) {
|
|
active := editor_active_buffer(editor)
|
|
if active == nil do return 0, 0, false
|
|
text_x := f32(editor_text_x(view))
|
|
if x < text_x || y < SDL_EDITOR_TEXT_Y do return 0, 0, false
|
|
line := view.first_line + int((y - SDL_EDITOR_TEXT_Y) / SDL_LINE_HEIGHT)
|
|
if line < 0 || line >= buffer_line_count(&active.buffer) do return 0, 0, false
|
|
column := text_column_from_pixel_x_gpu(gpu, &active.buffer, line, x - text_x)
|
|
return line, max_int(column, 0), true
|
|
}
|
|
|
|
editor_offset_from_mouse :: proc(editor: ^Editor, view: ^SDL_View, gpu: ^GPU_Renderer, x, y: f32) -> (int, bool) {
|
|
active := editor_active_buffer(editor)
|
|
if active == nil do return 0, false
|
|
line, column, ok := editor_line_col_from_mouse(editor, view, gpu, x, y)
|
|
if !ok do return 0, false
|
|
return buffer_line_col_to_offset(&active.buffer, line, column), true
|
|
}
|
|
|
|
handle_editor_right_click :: proc(menu: ^Context_Menu, editor: ^Editor, view: ^SDL_View, gpu: ^GPU_Renderer, tasks_panel: ^Gradle_Tasks_Panel, daemon: ^Daemon_Client, x, y: f32) -> bool {
|
|
if !editor_text_hit(view, tasks_panel, x, y) {
|
|
menu.open = false
|
|
return false
|
|
}
|
|
active := editor_active_buffer(editor)
|
|
if active == nil || active.diff != nil do return false
|
|
|
|
// Keep the selection when right-clicking inside it; otherwise move the
|
|
// caret to the click, matching IDE behavior.
|
|
offset, has_offset := editor_offset_from_mouse(editor, view, gpu, x, y)
|
|
start, end, has_selection := editor_selection_range(active)
|
|
if !(has_selection && has_offset && offset >= start && offset < end) {
|
|
_ = handle_editor_text_mouse(editor, view, gpu, x, y, false)
|
|
}
|
|
context_menu_open_at(menu, view, daemon, x, y)
|
|
return true
|
|
}
|
|
|
|
render_context_menu_gpu :: proc(gpu: ^GPU_Renderer, view: ^SDL_View, menu: ^Context_Menu, editor: ^Editor) {
|
|
if !menu.open do return
|
|
|
|
height := context_menu_height()
|
|
gpu_rect(gpu, menu.x, menu.y, SDL_CONTEXT_MENU_WIDTH, height, 25, 28, 36, 255)
|
|
gpu_rect_outline(gpu, menu.x, menu.y, SDL_CONTEXT_MENU_WIDTH, height, 58, 62, 72, 255)
|
|
|
|
hovered_index, has_hover := context_menu_item_at(menu, view.mouse_x, view.mouse_y)
|
|
row_y := menu.y + SDL_CONTEXT_MENU_PADDING
|
|
for item, index in context_menu_items {
|
|
if item.separator_before {
|
|
gpu_rect(gpu, menu.x + 8, row_y + 3, SDL_CONTEXT_MENU_WIDTH - 16, 1, 52, 55, 63, 255)
|
|
row_y += SDL_CONTEXT_MENU_SEPARATOR
|
|
}
|
|
enabled := context_menu_item_enabled(menu, editor, item.action)
|
|
if enabled && has_hover && hovered_index == index {
|
|
gpu_rect(gpu, menu.x + 4, row_y, SDL_CONTEXT_MENU_WIDTH - 8, SDL_CONTEXT_MENU_ROW_HEIGHT, 49, 56, 70, 255)
|
|
}
|
|
if enabled {
|
|
gpu_text(gpu, menu.x + 14, row_y + 4, item.label, 220, 223, 228, 255)
|
|
} else {
|
|
gpu_text(gpu, menu.x + 14, row_y + 4, item.label, 120, 124, 132, 255)
|
|
}
|
|
row_y += SDL_CONTEXT_MENU_ROW_HEIGHT
|
|
}
|
|
}
|
|
|
|
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, dialog: ^Folder_Dialog_State,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, dialog, 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 && key == SDL.K_ESCAPE {
|
|
tasks_panel.open = false
|
|
view.gradle_sidebar_visible = false
|
|
ui_state_save(view)
|
|
return false
|
|
}
|
|
|
|
// Diff tabs are read-only: allow closing, the palette, and scrolling;
|
|
// swallow everything else so nothing edits the hidden buffer.
|
|
if active.diff != nil {
|
|
ctrl := (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE
|
|
shift := (mod & SDL.KMOD_SHIFT) != SDL.KMOD_NONE
|
|
switch {
|
|
case key == SDL.K_W && ctrl:
|
|
if editor_close_active(editor) {
|
|
scroll_sdl_view(editor, view, 0)
|
|
}
|
|
case key == SDL.K_P && ctrl && shift:
|
|
command_palette_open(command_palette)
|
|
case key == SDL.K_UP:
|
|
diff_view_scroll(active, view, -1)
|
|
case key == SDL.K_DOWN:
|
|
diff_view_scroll(active, view, 1)
|
|
case key == SDL.K_PAGEUP:
|
|
diff_view_scroll(active, view, -visible_editor_lines(view))
|
|
case key == SDL.K_PAGEDOWN:
|
|
diff_view_scroll(active, view, visible_editor_lines(view))
|
|
case key == SDL.K_HOME && ctrl:
|
|
active.diff.scroll = 0
|
|
case key == SDL.K_END && ctrl:
|
|
diff_view_scroll(active, view, len(active.diff.rows))
|
|
}
|
|
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_O && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE {
|
|
folder_dialog_show(dialog, workspace)
|
|
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
|
|
if active.diff != nil {
|
|
diff_view_scroll(active, view, delta)
|
|
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 := editor_content_bottom(view) - SDL_EDITOR_TEXT_Y - 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, context_menu: ^Context_Menu, tasks_panel: ^Gradle_Tasks_Panel, terminal: ^Terminal_Panel, git: ^Git_Panel, git_status: ^Git_Status_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_left_sidebar_gpu(gpu, view, tree, git_status, active.path)
|
|
|
|
editor_height := f32(editor_content_bottom(view) - SDL_EDITOR_TOP)
|
|
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)
|
|
|
|
if active.diff != nil {
|
|
render_diff_view_gpu(gpu, view, active, tasks_panel)
|
|
} else {
|
|
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_terminal_panel_gpu(gpu, view, terminal, git, tasks_panel)
|
|
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_tool_strip_gpu(gpu, tasks_panel, terminal)
|
|
render_command_palette_gpu(gpu, view, command_palette)
|
|
render_context_menu_gpu(gpu, view, context_menu, editor)
|
|
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 := content_right_edge(gpu.width) - 16
|
|
if tasks_panel != nil && tasks_panel.open {
|
|
right_limit = content_right_edge(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)
|
|
}
|
|
}
|
|
|
|
SDL_TOOLBAR_OPEN_FOLDER_LABEL :: "Open Folder"
|
|
|
|
// Geometry is shared by rendering, click handling, and hover cursor so the
|
|
// three always agree.
|
|
toolbar_open_folder_button :: proc() -> (x, y, width, height: f32) {
|
|
x = 14 + f32(gpu_text_width("Native Kotlin Editor")) + 26
|
|
y = 5
|
|
width = f32(gpu_text_width(SDL_TOOLBAR_OPEN_FOLDER_LABEL)) + 20
|
|
height = SDL_TOP_BAR_HEIGHT - 10
|
|
return
|
|
}
|
|
|
|
toolbar_open_folder_hit :: proc(x, y: f32) -> bool {
|
|
button_x, button_y, width, height := toolbar_open_folder_button()
|
|
return x >= button_x && x < button_x + width && y >= button_y && y < button_y + height
|
|
}
|
|
|
|
handle_toolbar_click :: proc(dialog: ^Folder_Dialog_State, workspace: string, x, y: f32) -> bool {
|
|
if !toolbar_open_folder_hit(x, y) do return false
|
|
folder_dialog_show(dialog, workspace)
|
|
return true
|
|
}
|
|
|
|
SDL_TOOL_STRIP_GRADLE_LABEL :: "Gradle"
|
|
|
|
tool_strip_gradle_button :: proc(window_width: int) -> (x, y, width, height: f32) {
|
|
x = f32(window_width - SDL_TOOL_STRIP_WIDTH)
|
|
y = SDL_TOP_BAR_HEIGHT + 8
|
|
width = SDL_TOOL_STRIP_WIDTH
|
|
height = f32(len(SDL_TOOL_STRIP_GRADLE_LABEL)) * 14 + 16
|
|
return
|
|
}
|
|
|
|
tool_strip_gradle_hit :: proc(window_width: int, x, y: f32) -> bool {
|
|
button_x, button_y, width, height := tool_strip_gradle_button(window_width)
|
|
return x >= button_x && x < button_x + width && y >= button_y && y < button_y + height
|
|
}
|
|
|
|
SDL_TOOL_STRIP_TERMINAL_LABEL :: "Term"
|
|
|
|
// Left tool strip: switches the sidebar between file navigation and the git
|
|
// staging view.
|
|
left_tab_label :: proc(tab: Left_Tab) -> string {
|
|
switch tab {
|
|
case .Files:
|
|
return "Files"
|
|
case .Git:
|
|
return "Git"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
left_strip_button :: proc(tab: Left_Tab) -> (x, y, width, height: f32) {
|
|
x = 0
|
|
y = SDL_TOP_BAR_HEIGHT + 8
|
|
width = SDL_TOOL_STRIP_WIDTH
|
|
for t in Left_Tab {
|
|
height = f32(len(left_tab_label(t))) * 14 + 16
|
|
if t == tab do break
|
|
y += height + 8
|
|
}
|
|
return
|
|
}
|
|
|
|
left_strip_hit :: proc(tab: Left_Tab, x, y: f32) -> bool {
|
|
button_x, button_y, width, height := left_strip_button(tab)
|
|
return x >= button_x && x < button_x + width && y >= button_y && y < button_y + height
|
|
}
|
|
|
|
left_tab_activate :: proc(view: ^SDL_View, tab: Left_Tab) {
|
|
if view.left_tab == tab {
|
|
view.explorer_visible = !view.explorer_visible
|
|
} else {
|
|
view.left_tab = tab
|
|
view.explorer_visible = true
|
|
}
|
|
}
|
|
|
|
handle_left_strip_click :: proc(view: ^SDL_View, git_status: ^Git_Status_Panel, workspace: string, x, y: f32) -> bool {
|
|
if x >= f32(SDL_TOOL_STRIP_WIDTH) do return false
|
|
if y < SDL_TOP_BAR_HEIGHT || y >= f32(view.window_height - SDL_STATUS_BAR_HEIGHT) do return false
|
|
if left_strip_hit(.Files, x, y) {
|
|
left_tab_activate(view, .Files)
|
|
ui_state_save(view)
|
|
} else if left_strip_hit(.Git, x, y) {
|
|
left_tab_activate(view, .Git)
|
|
if git_status_panel_active(view) {
|
|
git_status_request(git_status, workspace)
|
|
}
|
|
ui_state_save(view)
|
|
}
|
|
// Clicks on the strip never fall through.
|
|
return true
|
|
}
|
|
|
|
// The whole left column: strip background, its buttons, and the active
|
|
// sidebar panel (file tree or git status).
|
|
render_left_sidebar_gpu :: proc(gpu: ^GPU_Renderer, view: ^SDL_View, tree: ^Project_Tree, git_status: ^Git_Status_Panel, active_path: string) {
|
|
height := f32(gpu.height - SDL_TOP_BAR_HEIGHT - SDL_STATUS_BAR_HEIGHT)
|
|
gpu_rect(gpu, 0, SDL_TOP_BAR_HEIGHT, SDL_TOOL_STRIP_WIDTH, height, 37, 38, 43, 255)
|
|
gpu_rect(gpu, SDL_TOOL_STRIP_WIDTH - 1, SDL_TOP_BAR_HEIGHT, 1, height, 56, 58, 64, 255)
|
|
tool_strip_button_render(gpu, left_strip_button(.Files), left_tab_label(.Files), view.explorer_visible && view.left_tab == .Files)
|
|
tool_strip_button_render(gpu, left_strip_button(.Git), left_tab_label(.Git), view.explorer_visible && view.left_tab == .Git)
|
|
|
|
if !view.explorer_visible do return
|
|
left := f32(content_left_edge())
|
|
sidebar_width := f32(left_sidebar_width(view))
|
|
gpu_rect(gpu, left, SDL_TOP_BAR_HEIGHT, sidebar_width - left, height, 35, 36, 40, 255)
|
|
gpu_rect(gpu, left, SDL_TOP_BAR_HEIGHT, sidebar_width - left, 1, 55, 57, 64, 255)
|
|
gpu_rect(gpu, sidebar_width - 2, SDL_TOP_BAR_HEIGHT, 4, height, 57, 60, 68, 255)
|
|
|
|
if view.left_tab == .Files {
|
|
render_project_tree_gpu(gpu, tree, view, active_path)
|
|
} else {
|
|
render_git_status_panel_gpu(gpu, view, git_status)
|
|
}
|
|
}
|
|
|
|
tool_strip_terminal_button :: proc(window_width: int) -> (x, y, width, height: f32) {
|
|
_, gradle_y, _, gradle_height := tool_strip_gradle_button(window_width)
|
|
x = f32(window_width - SDL_TOOL_STRIP_WIDTH)
|
|
y = gradle_y + gradle_height + 8
|
|
width = SDL_TOOL_STRIP_WIDTH
|
|
height = f32(len(SDL_TOOL_STRIP_TERMINAL_LABEL)) * 14 + 16
|
|
return
|
|
}
|
|
|
|
tool_strip_terminal_hit :: proc(window_width: int, x, y: f32) -> bool {
|
|
button_x, button_y, width, height := tool_strip_terminal_button(window_width)
|
|
return x >= button_x && x < button_x + width && y >= button_y && y < button_y + height
|
|
}
|
|
|
|
handle_tool_strip_click :: proc(view: ^SDL_View, tasks_panel: ^Gradle_Tasks_Panel, terminal: ^Terminal_Panel, git: ^Git_Panel, workspace: string, editor: ^Editor, daemon: ^Daemon_Client, sync: ^Daemon_Sync_State, x, y: f32) -> bool {
|
|
if x < f32(content_right_edge(view.window_width)) do return false
|
|
if y < SDL_TOP_BAR_HEIGHT || y >= f32(view.window_height - SDL_STATUS_BAR_HEIGHT) do return false
|
|
if tool_strip_gradle_hit(view.window_width, x, y) {
|
|
gradle_panel_toggle(view, tasks_panel, editor, daemon, sync)
|
|
} else if tool_strip_terminal_hit(view.window_width, x, y) {
|
|
terminal_toggle(terminal, git, view, workspace)
|
|
}
|
|
// Clicks on the strip never fall through to the editor behind it.
|
|
return true
|
|
}
|
|
|
|
render_tool_strip_gpu :: proc(gpu: ^GPU_Renderer, tasks_panel: ^Gradle_Tasks_Panel, terminal: ^Terminal_Panel) {
|
|
x := f32(content_right_edge(gpu.width))
|
|
y: f32 = SDL_TOP_BAR_HEIGHT
|
|
height := f32(gpu.height - SDL_TOP_BAR_HEIGHT - SDL_STATUS_BAR_HEIGHT)
|
|
gpu_rect(gpu, x, y, SDL_TOOL_STRIP_WIDTH, height, 37, 38, 43, 255)
|
|
gpu_rect(gpu, x, y, 1, height, 56, 58, 64, 255)
|
|
|
|
tool_strip_button_render(gpu, tool_strip_gradle_button(gpu.width), SDL_TOOL_STRIP_GRADLE_LABEL, tasks_panel.open)
|
|
tool_strip_button_render(gpu, tool_strip_terminal_button(gpu.width), SDL_TOOL_STRIP_TERMINAL_LABEL, terminal.open)
|
|
}
|
|
|
|
tool_strip_button_render :: proc(gpu: ^GPU_Renderer, button_x, button_y, button_width, button_height: f32, label: string, active: bool) {
|
|
if active {
|
|
gpu_rect(gpu, button_x + 2, button_y, button_width - 3, button_height, 58, 64, 82, 255)
|
|
}
|
|
char_x := button_x + (button_width - 8) * 0.5
|
|
for i in 0 ..< len(label) {
|
|
gpu_text(gpu, char_x, button_y + 8 + f32(i) * 14, label[i:i + 1], 200, 206, 216, 255)
|
|
}
|
|
}
|
|
|
|
render_command_palette_gpu :: proc(gpu: ^GPU_Renderer, view: ^SDL_View, 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
|
|
row_hovered := view.mouse_x >= x + 8 && view.mouse_x < x + width - 8 && view.mouse_y >= item_y - 5 && view.mouse_y < item_y + 17
|
|
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)
|
|
} else if row_hovered {
|
|
gpu_rect(gpu, x + 8, item_y - 5, width - 16, 22, 38, 42, 52, 255)
|
|
}
|
|
color := [3]u8{218, 222, 230}
|
|
if seen != palette.selected {
|
|
color = {166, 172, 184}
|
|
}
|
|
if row_hovered && seen != palette.selected {
|
|
color = {200, 205, 215}
|
|
}
|
|
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(editor_content_bottom(view)) - 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]
|
|
row_hovered := view.mouse_x >= x + 8 && view.mouse_x < x + width - 8 && view.mouse_y >= item_y - 4 && view.mouse_y < item_y + 14
|
|
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)
|
|
} else if row_hovered {
|
|
gpu_rect(gpu, x + 8, item_y - 4, width - 16, 20, 40, 43, 50, 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
|
|
|
|
x, y, width, height := gradle_panel_geometry(view, gpu.width, gpu.height)
|
|
gpu_rect(gpu, x, y, width, height, 35, 36, 40, 255)
|
|
gpu_rect(gpu, x, y, 1, height, 56, 58, 64, 255)
|
|
gpu_text(gpu, x + 14, y + 12, "GRADLE", 150, 154, 162, 255)
|
|
task_count := fmt.tprintf("%d", len(panel.items))
|
|
gpu_text_limited(gpu, x + width - 44, y + 12, task_count, 5, 150, 154, 162, 255)
|
|
|
|
if len(panel.message) > 0 {
|
|
gpu_text_limited(gpu, x + 14, y + 32, panel.message, int((width - 28) / 8), 150, 154, 162, 255)
|
|
}
|
|
if len(panel.items) == 0 {
|
|
gpu_text_limited(gpu, x + 14, y + SDL_GRADLE_LIST_TOP, "No Gradle tasks loaded", 34, 150, 154, 162, 255)
|
|
}
|
|
|
|
output_height := gradle_panel_output_height(panel, height)
|
|
rows := gradle_panel_rows(panel)
|
|
visible := gradle_panel_visible_rows(panel, height)
|
|
panel.scroll = clamp_int(panel.scroll, 0, max_int(len(rows) - visible, 0))
|
|
max_columns := max_int(int((width - 44) / max_f32(gpu.font_advance, 1)), 1)
|
|
row_y := y + SDL_GRADLE_LIST_TOP
|
|
for offset in 0 ..< visible {
|
|
index := panel.scroll + offset
|
|
if index >= len(rows) do break
|
|
row := rows[index]
|
|
row_hovered := view.mouse_x >= x + 4 && view.mouse_x < x + width - 4 && view.mouse_y >= row_y && view.mouse_y < row_y + SDL_GRADLE_ROW_HEIGHT
|
|
if row.kind == .Task && row.item_index == panel.selected {
|
|
gpu_rect(gpu, x + 4, row_y, width - 8, SDL_GRADLE_ROW_HEIGHT, 49, 56, 70, 255)
|
|
gpu_rect(gpu, x + 4, row_y, 2, SDL_GRADLE_ROW_HEIGHT, 77, 155, 230, 255)
|
|
} else if row_hovered {
|
|
gpu_rect(gpu, x + 4, row_y, width - 8, SDL_GRADLE_ROW_HEIGHT, 42, 44, 50, 255)
|
|
}
|
|
text_y := row_y + 2
|
|
switch row.kind {
|
|
case .Project:
|
|
arrow := ">" if panel.collapsed[row.key] else "v"
|
|
gpu_text(gpu, x + 12, text_y, arrow, 140, 146, 158, 255)
|
|
gpu_text_limited(gpu, x + 24, text_y, row.label, max_columns, 200, 205, 215, 255)
|
|
case .Group:
|
|
arrow := ">" if panel.collapsed[row.key] else "v"
|
|
gpu_text(gpu, x + 26, text_y, arrow, 140, 146, 158, 255)
|
|
gpu_text_limited(gpu, x + 38, text_y, row.label, max_columns, 170, 176, 188, 255)
|
|
case .Task:
|
|
gpu_text_limited(gpu, x + 52, text_y, row.label, max_columns, 218, 222, 230, 255)
|
|
}
|
|
row_y += SDL_GRADLE_ROW_HEIGHT
|
|
}
|
|
|
|
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)
|
|
first_line := hover.contents
|
|
if newline := strings.index_byte(first_line, '\n'); newline >= 0 {
|
|
first_line = first_line[:newline]
|
|
}
|
|
text := fmt.ctprintf("%s", first_line)
|
|
_ = 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
|
|
|
|
// Hover contents can span several lines (signatures, docs), so the box
|
|
// is sized to the visible lines and each line is drawn separately —
|
|
// gpu_text would otherwise continue multi-line text below the box.
|
|
all_lines, _ := strings.split_lines(hover.contents, context.temp_allocator)
|
|
line_count := min_int(len(all_lines), 12)
|
|
lines := all_lines[:line_count]
|
|
for len(lines) > 0 && len(strings.trim_space(lines[len(lines) - 1])) == 0 {
|
|
lines = lines[:len(lines) - 1]
|
|
}
|
|
if len(lines) == 0 do return
|
|
|
|
max_line_width := 0
|
|
for line in lines {
|
|
max_line_width = max_int(max_line_width, gpu_font_text_width(gpu, line))
|
|
}
|
|
width := f32(max_int(180, min_int(560, max_line_width + 20)))
|
|
height := f32(18 + len(lines) * 16)
|
|
|
|
anchor_line := min_int(hover.line, buffer_line_count(&active.buffer) - 1)
|
|
anchor_col := hover.word_start if hover.from_mouse else hover.column
|
|
x := editor_column_pixel_x_gpu(gpu, view, &active.buffer, anchor_line, anchor_col)
|
|
y := f32(SDL_EDITOR_TEXT_Y + (anchor_line - view.first_line + 2) * SDL_LINE_HEIGHT)
|
|
x = max_f32(min_f32(x, f32(content_right_edge(gpu.width)) - width - 4), 0)
|
|
y = max_f32(min_f32(y, f32(editor_content_bottom(view)) - height - 4), SDL_EDITOR_TOP)
|
|
|
|
gpu_rect(gpu, x, y, width, height, 30, 34, 45, 255)
|
|
gpu_rect_outline(gpu, x, y, width, height, 100, 125, 165, 255)
|
|
max_columns := max_int(int((width - 20) / max_f32(gpu.font_advance, 1)), 1)
|
|
line_y := y + 10
|
|
for line in lines {
|
|
gpu_text_limited(gpu, x + 10, line_y, line, max_columns, 220, 225, 235, 255)
|
|
line_y += 16
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
marker := " "
|
|
if item.is_dir {
|
|
marker = "v " if item.expanded else "> "
|
|
}
|
|
label := fmt.ctprintf("%s%s", marker, item.name)
|
|
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 + f32(item.depth * 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 || view.left_tab != .Files do return
|
|
left := f32(content_left_edge())
|
|
sidebar_width := left_sidebar_width(view)
|
|
gpu_text(gpu, left + 14, SDL_TREE_HEADER_Y, "EXPLORER", 150, 154, 162, 255)
|
|
gpu_rect(gpu, left + 14, SDL_TREE_HEADER_Y + 18, f32(sidebar_width) - left - 28, 1, 52, 54, 60, 255)
|
|
|
|
hovered_index, has_hover := project_tree_row_at(tree, view, view.mouse_x, view.mouse_y)
|
|
|
|
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]
|
|
is_active := item.path == active_path
|
|
if is_active {
|
|
gpu_rect(gpu, left + 8, y - 3, f32(sidebar_width) - left - 16, SDL_TREE_ROW_HEIGHT + 1, 49, 51, 58, 255)
|
|
gpu_rect(gpu, left + 8, y - 3, 2, SDL_TREE_ROW_HEIGHT + 1, 77, 155, 230, 255)
|
|
} else if has_hover && index == hovered_index {
|
|
gpu_rect(gpu, left + 8, y - 3, f32(sidebar_width) - left - 16, SDL_TREE_ROW_HEIGHT + 1, 42, 44, 50, 255)
|
|
}
|
|
|
|
indent := left + f32(16 + item.depth * 12)
|
|
text_budget := f32(sidebar_width) - indent - 24
|
|
max_columns := max_int(int(text_budget / max_f32(gpu.font_advance, 1)), 1)
|
|
if item.is_dir {
|
|
arrow := "v" if item.expanded else ">"
|
|
gpu_text(gpu, indent, y, arrow, 140, 146, 158, 255)
|
|
gpu_text_limited(gpu, indent + gpu.font_advance * 2, y, item.name, max_int(max_columns - 2, 1), 200, 205, 214, 255)
|
|
} else if is_active {
|
|
gpu_text_limited(gpu, indent + gpu.font_advance * 2, y, item.name, max_int(max_columns - 2, 1), 235, 237, 241, 255)
|
|
} else {
|
|
gpu_text_limited(gpu, indent + gpu.font_advance * 2, y, item.name, max_int(max_columns - 2, 1), 185, 189, 198, 255)
|
|
}
|
|
y += SDL_TREE_ROW_HEIGHT
|
|
}
|
|
|
|
// Scrollbar when the tree overflows the sidebar.
|
|
total := len(tree.files)
|
|
visible := visible_tree_rows(view)
|
|
if total > visible {
|
|
track_top := f32(SDL_TREE_FIRST_Y - 4)
|
|
track_height := f32(visible * SDL_TREE_ROW_HEIGHT)
|
|
track_x := f32(sidebar_width - 8)
|
|
gpu_rect(gpu, track_x, track_top, 4, track_height, 40, 42, 48, 255)
|
|
thumb_height := max_f32(track_height * f32(visible) / f32(total), 24)
|
|
max_first := max_int(total - visible, 1)
|
|
thumb_y := track_top + (track_height - thumb_height) * f32(view.tree_first) / f32(max_first)
|
|
gpu_rect(gpu, track_x, thumb_y, 4, thumb_height, 92, 96, 106, 255)
|
|
}
|
|
}
|
|
|
|
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)
|
|
mouse_in_bar := view.mouse_y >= SDL_TOP_BAR_HEIGHT && view.mouse_y < SDL_EDITOR_TOP
|
|
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
|
|
hovered := mouse_in_bar && view.mouse_x >= tab_x && view.mouse_x < tab_x + width
|
|
close_hovered := hovered && view.mouse_x >= tab_x + width - 28
|
|
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 if hovered {
|
|
gpu_rect(gpu, tab_x, SDL_TOP_BAR_HEIGHT + 6, width, SDL_TAB_BAR_HEIGHT - 6, 48, 50, 56, 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)
|
|
title_color := [3]u8{220, 223, 228} if active else [3]u8{168, 172, 182}
|
|
if hovered && !active {
|
|
title_color = {198, 202, 211}
|
|
}
|
|
gpu_text_limited(gpu, tab_x + 16, SDL_TOP_BAR_HEIGHT + 12, tab_title, 34, title_color[0], title_color[1], title_color[2], 255)
|
|
if close_hovered {
|
|
gpu_rect(gpu, tab_x + width - 27, SDL_TOP_BAR_HEIGHT + 8, 15, SDL_TAB_BAR_HEIGHT - 14, 62, 65, 73, 255)
|
|
gpu_text(gpu, tab_x + width - 22, SDL_TOP_BAR_HEIGHT + 12, "x", 235, 237, 241, 255)
|
|
} else {
|
|
close_color := [3]u8{168, 172, 182} if active else [3]u8{124, 128, 138}
|
|
gpu_text(gpu, tab_x + width - 22, SDL_TOP_BAR_HEIGHT + 12, "x", close_color[0], close_color[1], close_color[2], 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)
|
|
|
|
button_x, button_y, button_width, button_height := toolbar_open_folder_button()
|
|
gpu_rect(gpu, button_x, button_y, button_width, button_height, 56, 58, 66, 255)
|
|
gpu_rect_outline(gpu, button_x, button_y, button_width, button_height, 72, 76, 86, 255)
|
|
gpu_text(gpu, button_x + 10, button_y + 5, SDL_TOOLBAR_OPEN_FOLDER_LABEL, 200, 206, 216, 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)
|
|
}
|