This commit is contained in:
pavel 2026-07-09 08:27:13 +02:00
commit 985aa16236
13 changed files with 1381 additions and 241 deletions

View file

@ -64,6 +64,7 @@ Project_Tree :: struct {
workspace: string,
files: [dynamic]Project_File,
expanded: map[string]bool,
truncated: bool,
}
// State for the system folder-selection dialog. SDL may invoke the dialog
@ -131,6 +132,16 @@ Hover_Tooltip :: struct {
line: int,
column: int,
contents: string,
definition_path: string,
definition_line: int,
definition_column: int,
has_definition: bool,
x, y: f32,
width, height: f32,
selecting: bool,
selection_active: bool,
selection_anchor: int,
selection_cursor: int,
// 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,
@ -168,14 +179,6 @@ References_Panel :: struct {
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,
@ -195,6 +198,7 @@ Command_Palette :: struct {
Command_Id :: enum {
Open_Folder,
New_File,
Reload_Workspace,
Save,
Save_All,
@ -218,6 +222,7 @@ Command_Item :: struct {
COMMAND_ITEMS := [?]Command_Item{
{.Open_Folder, "Open Folder"},
{.New_File, "New File"},
{.Reload_Workspace, "Reload Workspace"},
{.Save, "Save File"},
{.Save_All, "Save All"},
@ -287,11 +292,6 @@ Syntax_State :: struct {
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
@ -307,7 +307,6 @@ 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
@ -484,22 +483,6 @@ 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)
@ -658,10 +641,10 @@ run_sdl_editor :: proc(initial_workspace: string, daemon_port: int, file_path: s
}
defer gradle_tasks_panel_destroy(&tasks_panel)
owned_daemon := Daemon_Process{}
defer daemon_stop_process(&owned_daemon)
daemon := Daemon_Client{}
defer daemon_close(&daemon)
owned_daemon := Daemon_Process{}
defer daemon_stop_process(&owned_daemon)
daemon_sync_state := Daemon_Sync_State{}
daemon_port_line: [dynamic]u8
defer delete(daemon_port_line)
@ -784,6 +767,8 @@ run_sdl_editor :: proc(initial_workspace: string, daemon_port: int, file_path: s
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_hover_tooltip_click(&hover, &navigation, &editor, &view, &gpu, 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) {
@ -797,6 +782,9 @@ run_sdl_editor :: proc(initial_workspace: string, daemon_port: int, file_path: s
} 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 {
if hover.open && len(hover.contents) > 0 {
hover.open = false
}
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)
@ -810,17 +798,17 @@ run_sdl_editor :: proc(initial_workspace: string, daemon_port: int, file_path: s
view.tab_dragging = false
view.tab_drag_index = 0
view.mouse_selecting = false
hover.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 handle_hover_tooltip_motion(&hover, &gpu, event.motion.x, event.motion.y) {
// Keep routing drag selection to the hover text.
} else if hover.open && hover.from_mouse && len(hover.contents) > 0 && !hover_tooltip_motion_safe(&hover, &editor, &view, &gpu, event.motion.x, event.motion.y) {
hover.open = false
}
if view.resizing_panel == .Bottom_Panel && (event.motion.state & SDL.BUTTON_LMASK) != {} {
handle_bottom_panel_resize_motion(&view, event.motion.y)
@ -889,6 +877,7 @@ run_sdl_editor :: proc(initial_workspace: string, daemon_port: int, file_path: s
if daemon.connected {
daemon_start_reader(&daemon)
daemon_send_workspace_open(&daemon, workspace)
editor_set_status(&editor, "Opening Kotlin 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)
@ -906,6 +895,7 @@ run_sdl_editor :: proc(initial_workspace: string, daemon_port: int, file_path: s
} else if !daemon_initialized {
daemon_start_reader(&daemon)
daemon_send_workspace_open(&daemon, workspace)
editor_set_status(&editor, "Opening Kotlin 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)
@ -914,6 +904,7 @@ run_sdl_editor :: proc(initial_workspace: string, daemon_port: int, file_path: s
}
daemon_sync_flush_if_due(&daemon_sync_state, &daemon, &editor)
daemon_apply_workspace_response(&daemon, &editor)
daemon_apply_latest_diagnostics(&daemon, &editor)
completion_popup_apply_response(&completion, &daemon)
hover_tooltip_apply_response(&hover, &daemon)
@ -992,6 +983,7 @@ completion_popup_open :: proc(popup: ^Completion_Popup, editor: ^Editor, daemon:
hover_tooltip_destroy :: proc(hover: ^Hover_Tooltip) {
delete(hover.contents)
delete(hover.definition_path)
}
hover_tooltip_open :: proc(hover: ^Hover_Tooltip, editor: ^Editor, daemon: ^Daemon_Client) {
@ -1003,6 +995,7 @@ hover_tooltip_open :: proc(hover: ^Hover_Tooltip, editor: ^Editor, daemon: ^Daem
hover.from_mouse = false
hover.line = line
hover.column = column
hover.has_definition = false
hover.pending_id = daemon_send_hover(daemon, active.path, line, column)
hover_tooltip_set_contents(hover, "Loading hover...")
}
@ -1040,6 +1033,7 @@ hover_tooltip_open_at_mouse :: proc(hover: ^Hover_Tooltip, editor: ^Editor, view
hover.column = column
hover.word_start = word_start
hover.word_end = word_end
hover.has_definition = false
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.
@ -1291,15 +1285,6 @@ references_panel_accept :: proc(panel: ^References_Panel, navigation: ^Navigatio
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)
@ -1383,6 +1368,8 @@ command_palette_accept :: proc(palette: ^Command_Palette, editor: ^Editor, view:
switch item.id {
case .Open_Folder:
folder_dialog_show(dialog, workspace)
case .New_File:
project_tree_create_new_file(editor, view, tree, workspace, daemon, sync)
case .Close_Tab:
if editor_close_active(editor) {
scroll_sdl_view(editor, view, 0)
@ -1425,6 +1412,31 @@ command_palette_accept :: proc(palette: ^Command_Palette, editor: ^Editor, view:
}
}
project_tree_create_new_file :: proc(editor: ^Editor, view: ^SDL_View, tree: ^Project_Tree, workspace: string, daemon: ^Daemon_Client, sync: ^Daemon_Sync_State) -> bool {
for index := 1; index < 1000; index += 1 {
name := "untitled.txt" if index == 1 else fmt.tprintf("untitled-%d.txt", index)
path := fmt.tprintf("%s/%s", workspace, name)
if os.is_file(path) do continue
err := os.write_entire_file(path, []u8{})
if err != nil {
editor_set_status(editor, fmt.tprintf("Could not create file: %s", path))
return false
}
if !editor_open_or_focus_file(editor, path) {
editor_set_status(editor, fmt.tprintf("Created file but could not open it: %s", path))
return false
}
project_tree_rebuild(tree)
scroll_project_tree(tree, view, 0)
daemon_sync_now(sync, daemon, editor, true)
editor_set_status(editor, fmt.tprintf("Created file: %s", path))
return true
}
editor_set_status(editor, "Could not choose a new file name")
return false
}
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
@ -1589,12 +1601,12 @@ parse_gradle_run_response :: proc(response: string, expected_id: int) -> (string
return "", false
}
if !message.ok {
code, _ := json_get_string(value, "code")
error_message, _ := json_get_string(value, "message")
error_message := protocol_error_message(value)
defer delete(error_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
return strings.clone("Gradle failed"), true
}
result, has_result := json_object_get(value, "result")
task := "task"
@ -1871,6 +1883,7 @@ attempt_open_folder :: proc(
// just needs the new root; only start a daemon if none is running.
if daemon.connected {
daemon_send_workspace_open(daemon, workspace^)
editor_set_status(editor, "Opening Kotlin workspace...")
daemon_sync_now(sync, daemon, editor, true)
} else if owned_enabled {
_ = daemon_begin_owned_start(workspace^, owned_daemon, port_line, editor, start_pending)
@ -1879,107 +1892,6 @@ attempt_open_folder :: proc(
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
@ -1987,8 +1899,11 @@ hover_tooltip_apply_response :: proc(hover: ^Hover_Tooltip, daemon: ^Daemon_Clie
defer delete(response)
if len(response) == 0 do return
contents, ok := parse_hover_response(string(response[:]), hover.pending_id)
defer delete(contents)
contents, definition_path, definition_line, definition_column, has_definition, ok := parse_hover_response(string(response[:]), hover.pending_id)
defer {
delete(contents)
delete(definition_path)
}
if !ok do return
hover.pending_id = 0
@ -2001,24 +1916,149 @@ hover_tooltip_apply_response :: proc(hover: ^Hover_Tooltip, daemon: ^Daemon_Clie
} else {
hover_tooltip_set_contents(hover, contents)
}
hover_tooltip_set_definition(hover, definition_path, definition_line, definition_column, has_definition)
}
hover_tooltip_set_contents :: proc(hover: ^Hover_Tooltip, contents: string) {
delete(hover.contents)
hover.contents = strings.clone(contents)
hover.selection_active = false
hover.selecting = false
hover.selection_anchor = 0
hover.selection_cursor = 0
}
parse_hover_response :: proc(response: string, expected_id: int) -> (string, bool) {
message, value, ok := parse_protocol_message(response)
if !ok do return "", false
hover_tooltip_set_definition :: proc(hover: ^Hover_Tooltip, path: string, line, column: int, has_definition: bool) {
delete(hover.definition_path)
hover.definition_path = strings.clone(path)
hover.definition_line = line
hover.definition_column = column
hover.has_definition = has_definition && len(path) > 0
}
parse_hover_response :: proc(response: string, expected_id: int) -> (contents: string, definition_path: string, definition_line: int, definition_column: int, has_definition: bool, ok: bool) {
message, value, parsed := parse_protocol_message(response)
if !parsed do return
defer json.destroy_value(value)
if message.kind != .Response || message.id != expected_id || !message.ok do return "", false
if message.kind != .Response || message.id != expected_id || !message.ok do return
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
if !has_result do return
parsed_contents, has_contents := json_get_string(result, "contents")
if has_contents {
contents = strings.clone(parsed_contents)
}
if definition, has_def := json_object_get(result, "definition"); has_def {
if path, has_path := json_get_string(definition, "path"); has_path {
definition_path = strings.clone(path)
definition_line, _ = json_get_int(definition, "line")
definition_column, _ = json_get_int(definition, "column")
has_definition = true
}
}
ok = true
return
}
hover_tooltip_hit :: proc(hover: ^Hover_Tooltip, x, y: f32) -> bool {
return hover.open && len(hover.contents) > 0 && x >= hover.x && x < hover.x + hover.width && y >= hover.y && y < hover.y + hover.height
}
hover_tooltip_motion_safe :: proc(hover: ^Hover_Tooltip, editor: ^Editor, view: ^SDL_View, gpu: ^GPU_Renderer, x, y: f32) -> bool {
if hover_tooltip_hit(hover, x, y) do return true
active := editor_active_buffer(editor)
if active == nil do return false
if hover.line < 0 || hover.line >= buffer_line_count(&active.buffer) do return false
word_x1 := editor_column_pixel_x_gpu(gpu, view, &active.buffer, hover.line, hover.word_start)
word_x2 := editor_column_pixel_x_gpu(gpu, view, &active.buffer, hover.line, hover.word_end)
word_y := f32(SDL_EDITOR_TEXT_Y + (hover.line - view.first_line) * SDL_LINE_HEIGHT)
if x >= word_x1 && x < word_x2 && y >= word_y - 3 && y < word_y + SDL_LINE_HEIGHT {
return true
}
// Let the pointer travel from the source word to the tooltip without the
// dwell hover disappearing in the gap between them.
left := min_f32(word_x1, hover.x) - 10
right := max_f32(word_x2, hover.x + hover.width) + 10
top := min_f32(word_y - 3, hover.y) - 10
bottom := max_f32(word_y + SDL_LINE_HEIGHT, hover.y + hover.height) + 10
return x >= left && x < right && y >= top && y < bottom
}
hover_tooltip_action_hit :: proc(hover: ^Hover_Tooltip, x, y: f32) -> bool {
if !hover.has_definition do return false
action_y := hover.y + hover.height - 25
return x >= hover.x + 8 && x < hover.x + 142 && y >= action_y && y < action_y + 18
}
hover_visible_lines :: proc(hover: ^Hover_Tooltip) -> []string {
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]
}
return lines
}
hover_line_start_offset :: proc(hover: ^Hover_Tooltip, line_index: int) -> int {
offset := 0
current := 0
for i := 0; i < len(hover.contents) && current < line_index; i += 1 {
offset += 1
if hover.contents[i] == '\n' {
current += 1
}
}
return offset
}
hover_text_offset_at :: proc(hover: ^Hover_Tooltip, gpu: ^GPU_Renderer, x, y: f32) -> int {
lines := hover_visible_lines(hover)
if len(lines) == 0 do return 0
line_index := clamp_int(int((y - (hover.y + 10)) / 16), 0, len(lines) - 1)
line := lines[line_index]
rel_x := max_f32(x - (hover.x + 10), 0)
column := int((rel_x + max_f32(gpu.font_advance, 1) * 0.5) / max_f32(gpu.font_advance, 1))
column = clamp_int(column, 0, len(line))
return min_int(hover_line_start_offset(hover, line_index) + column, len(hover.contents))
}
hover_tooltip_copy_selection :: proc(hover: ^Hover_Tooltip) -> bool {
if !hover.selection_active do return false
start := min_int(hover.selection_anchor, hover.selection_cursor)
end := max_int(hover.selection_anchor, hover.selection_cursor)
if start == end do return false
c_text, err := strings.clone_to_cstring(hover.contents[start:end], context.temp_allocator)
if err != nil do return false
return SDL.SetClipboardText(c_text)
}
handle_hover_tooltip_click :: proc(hover: ^Hover_Tooltip, navigation: ^Navigation_History, editor: ^Editor, view: ^SDL_View, gpu: ^GPU_Renderer, x, y: f32) -> bool {
if !hover_tooltip_hit(hover, x, y) do return false
if hover_tooltip_action_hit(hover, x, y) {
active := editor_active_buffer(editor)
if active != nil {
navigation_push(&navigation.back, active.path, active.cursor.offset)
navigation_clear_stack(&navigation.forward)
}
_ = editor_jump_current_tab_to_location(editor, view, hover.definition_path, hover.definition_line, hover.definition_column)
hover.open = false
return true
}
offset := hover_text_offset_at(hover, gpu, x, y)
hover.selecting = true
hover.selection_active = true
hover.selection_anchor = offset
hover.selection_cursor = offset
return true
}
handle_hover_tooltip_motion :: proc(hover: ^Hover_Tooltip, gpu: ^GPU_Renderer, x, y: f32) -> bool {
if !hover.selecting do return false
hover.selection_cursor = hover_text_offset_at(hover, gpu, x, y)
return true
}
completion_popup_apply_response :: proc(popup: ^Completion_Popup, daemon: ^Daemon_Client) {
@ -2160,6 +2200,7 @@ project_tree_clear_files :: proc(tree: ^Project_Tree) {
project_tree_rebuild :: proc(tree: ^Project_Tree) {
project_tree_clear_files(tree)
tree.truncated = false
project_tree_append_dir(tree, tree.workspace, 0)
}
@ -2194,12 +2235,16 @@ workspace_reload :: proc(editor: ^Editor, view: ^SDL_View, tree: ^Project_Tree,
scroll_project_tree(tree, view, 0)
editor_clear_all_diagnostics(editor)
daemon_send_workspace_open(daemon, workspace)
editor_set_status(editor, "Opening Kotlin 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
if depth > 12 do return
if len(tree.files) >= 5000 {
tree.truncated = true
return
}
entries, err := os.read_all_directory_by_path(dir, context.allocator)
if err != nil do return
@ -2213,7 +2258,10 @@ project_tree_append_dir :: proc(tree: ^Project_Tree, dir: string, depth: int) {
})
for entry in entries {
if len(tree.files) >= 5000 do return
if len(tree.files) >= 5000 {
tree.truncated = true
return
}
if project_tree_skip(entry.name) do continue
path := fmt.tprintf("%s/%s", dir, entry.name)
@ -3011,6 +3059,10 @@ handle_sdl_key :: proc(editor: ^Editor, view: ^SDL_View, tree: ^Project_Tree, wo
active := editor_active_buffer(editor)
if active == nil do return false
if key == SDL.K_C && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE && hover_tooltip_copy_selection(hover) {
return false
}
if command_palette.open {
switch key {
case SDL.K_ESCAPE:
@ -3052,8 +3104,12 @@ handle_sdl_key :: proc(editor: ^Editor, view: ^SDL_View, tree: ^Project_Tree, wo
rename_panel_backspace(rename)
return false
case SDL.K_RETURN:
daemon_sync_now(sync, daemon, editor, false)
rename_panel_request(rename, editor, daemon)
if len(rename.edits) > 0 {
_ = rename_panel_apply_edits(rename, editor, sync)
} else {
daemon_sync_now(sync, daemon, editor, false)
rename_panel_request(rename, editor, daemon)
}
return false
}
}
@ -3352,7 +3408,8 @@ handle_sdl_key :: proc(editor: ^Editor, view: ^SDL_View, tree: ^Project_Tree, wo
switch key {
case SDL.K_ESCAPE:
// Window close is handled by the window manager; escape currently only clears intent.
hover.open = false
return false
case SDL.K_RETURN:
editor_insert_newline_auto_indent(active)
close_stale_edit_overlays(completion, hover, references, rename)
@ -4096,7 +4153,7 @@ render_rename_panel :: proc(renderer: ^SDL.Renderer, panel: ^Rename_Panel) {
item_y := y + 68
for edit, index in panel.edits {
if index >= 8 do break
label := fmt.ctprintf("%s", edit)
label := fmt.ctprintf("%s", edit.label)
_ = SDL.RenderDebugText(renderer, x + 10, item_y, label)
item_y += 16
}
@ -4122,7 +4179,7 @@ render_rename_panel_gpu :: proc(gpu: ^GPU_Renderer, panel: ^Rename_Panel) {
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)
gpu_text_limited(gpu, x + 10, item_y, edit.label, 56, 220, 225, 235, 255)
item_y += 16
}
}
@ -4597,12 +4654,7 @@ render_hover_tooltip_gpu :: proc(gpu: ^GPU_Renderer, editor: ^Editor, view: ^SDL
// 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]
}
lines := hover_visible_lines(hover)
if len(lines) == 0 do return
max_line_width := 0
@ -4610,7 +4662,8 @@ render_hover_tooltip_gpu :: proc(gpu: ^GPU_Renderer, editor: ^Editor, view: ^SDL
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)
action_height := 26 if hover.has_definition else 0
height := f32(18 + len(lines) * 16 + action_height)
anchor_line := min_int(hover.line, buffer_line_count(&active.buffer) - 1)
anchor_col := hover.word_start if hover.from_mouse else hover.column
@ -4618,15 +4671,42 @@ render_hover_tooltip_gpu :: proc(gpu: ^GPU_Renderer, editor: ^Editor, view: ^SDL
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)
hover.x = x
hover.y = y
hover.width = width
hover.height = height
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
selection_start := min_int(hover.selection_anchor, hover.selection_cursor)
selection_end := max_int(hover.selection_anchor, hover.selection_cursor)
content_offset := 0
syntax_state := Syntax_State{}
for line in lines {
gpu_text_limited(gpu, x + 10, line_y, line, max_columns, 220, 225, 235, 255)
visible_len := min_int(len(line), max_columns)
if hover.selection_active && selection_start != selection_end {
line_start := content_offset
line_end := content_offset + visible_len
if selection_end > line_start && selection_start < line_end {
start_col := clamp_int(selection_start - line_start, 0, visible_len)
end_col := clamp_int(selection_end - line_start, start_col, visible_len)
if end_col > start_col {
gpu_rect(gpu, x + 10 + f32(start_col) * gpu.font_advance, line_y - 2, f32(end_col - start_col) * gpu.font_advance, 16, 62, 83, 125, 190)
}
}
}
syntax_state = render_syntax_line_gpu(gpu, x + 10, line_y, line, max_columns, syntax_state)
content_offset += len(line) + 1
line_y += 16
}
if hover.has_definition {
action_y := y + height - 25
gpu_rect(gpu, x + 8, action_y, 134, 18, 43, 54, 72, 255)
gpu_rect_outline(gpu, x + 8, action_y, 134, 18, 77, 155, 230, 255)
gpu_text(gpu, x + 16, action_y + 4, "Go to definition", 160, 205, 255, 255)
}
}
render_completion_popup :: proc(renderer: ^SDL.Renderer, editor: ^Editor, view: ^SDL_View, popup: ^Completion_Popup) {
@ -4808,7 +4888,8 @@ render_project_tree :: proc(renderer: ^SDL.Renderer, tree: ^Project_Tree, view:
_ = SDL.RenderFillRect(renderer, &sidebar)
_ = SDL.SetRenderDrawColor(renderer, 145, 165, 205, 255)
_ = SDL.RenderDebugText(renderer, 12, 8, "Project")
title := "Project (truncated)" if tree.truncated else "Project"
_ = SDL.RenderDebugText(renderer, 12, 8, fmt.ctprintf("%s", title))
y: f32 = 28
end := min_int(view.tree_first + visible_tree_rows(view), len(tree.files))
@ -4841,7 +4922,8 @@ render_project_tree_gpu :: proc(gpu: ^GPU_Renderer, tree: ^Project_Tree, view: ^
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)
title := "EXPLORER (TRUNCATED)" if tree.truncated else "EXPLORER"
gpu_text(gpu, left + 14, SDL_TREE_HEADER_Y, title, 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)