package main import "core:fmt" import "core:os" import "core:strings" import json "core:encoding/json" Rename_Edit :: struct { path: string, line: int, column: int, old_text: string, new_text: string, label: string, } Rename_File_State :: struct { path: string, original_storage: []u8, buffer: Buffer, } Rename_Panel :: struct { open: bool, pending_id: int, input: [dynamic]u8, summary: string, edits: [dynamic]Rename_Edit, } rename_panel_destroy :: proc(panel: ^Rename_Panel) { delete(panel.input) delete(panel.summary) rename_panel_clear_edits(panel) delete(panel.edits) } rename_panel_clear_edits :: proc(panel: ^Rename_Panel) { for edit in panel.edits { delete(edit.path) delete(edit.old_text) delete(edit.new_text) delete(edit.label) } clear(&panel.edits) } 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") rename_panel_clear_edits(panel) } rename_panel_insert :: proc(panel: ^Rename_Panel, text: string) { rename_panel_clear_edits(panel) 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) { rename_panel_clear_edits(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 { rename_panel_set_summary(panel, "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[:])) rename_panel_set_summary(panel, "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) rename_edits_destroy(edits[:]) delete(edits) } if !ok do return panel.pending_id = 0 rename_panel_set_summary(panel, summary) rename_panel_clear_edits(panel) for edit in edits { append(&panel.edits, rename_edit_clone(edit)) } } rename_panel_apply_edits :: proc(panel: ^Rename_Panel, editor: ^Editor, sync: ^Daemon_Sync_State) -> bool { if !panel.open || len(panel.edits) == 0 do return false file_states: [dynamic]Rename_File_State defer rename_file_states_destroy(file_states[:]) defer delete(file_states) for edit in panel.edits { buffer := editor_buffer_for_rename_edit(editor, edit.path) target_buffer: ^Buffer if buffer != nil { if buffer.dirty { rename_panel_set_summary(panel, "Save affected buffers before applying rename") return false } target_buffer = &buffer.buffer } else { file_state := rename_file_state_for_path(&file_states, edit.path) if file_state == nil { rename_panel_set_summary(panel, fmt.tprintf("Could not read rename target: %s", edit.path)) return false } target_buffer = &file_state.buffer } offset := buffer_line_col_to_offset(target_buffer, max_int(edit.line - 1, 0), max_int(edit.column - 1, 0)) existing := buffer_range_bytes(target_buffer, offset, len(edit.old_text)) matches := bytes_equal(existing[:], transmute([]u8)edit.old_text) delete(existing) if !matches { rename_panel_set_summary(panel, "Rename preview is stale; request it again") return false } } for index := len(panel.edits) - 1; index >= 0; index -= 1 { edit := panel.edits[index] buffer := editor_buffer_for_rename_edit(editor, edit.path) if buffer != nil { offset := buffer_line_col_to_offset(&buffer.buffer, max_int(edit.line - 1, 0), max_int(edit.column - 1, 0)) buffer_replace_range(&buffer.buffer, offset, len(edit.old_text), edit.new_text) buffer.cursor.offset = offset + len(edit.new_text) _, buffer.cursor.wanted_column = buffer_offset_to_line_col(&buffer.buffer, buffer.cursor.offset) editor_clear_selection(buffer) editor_update_dirty(buffer) } else if file_state := rename_file_state_find(file_states[:], edit.path); file_state != nil { offset := buffer_line_col_to_offset(&file_state.buffer, max_int(edit.line - 1, 0), max_int(edit.column - 1, 0)) buffer_replace_range(&file_state.buffer, offset, len(edit.old_text), edit.new_text) } } for &file_state in file_states { text := buffer_bytes(&file_state.buffer) err := os.write_entire_file(file_state.path, text[:]) delete(text) if err != nil { rename_panel_set_summary(panel, fmt.tprintf("Could not write rename target: %s", file_state.path)) return false } } applied := len(panel.edits) rename_panel_clear_edits(panel) panel.open = false editor_set_status(editor, fmt.tprintf("Applied rename preview (%d edits)", applied)) daemon_sync_schedule(sync) return true } rename_file_state_for_path :: proc(states: ^[dynamic]Rename_File_State, path: string) -> ^Rename_File_State { if existing := rename_file_state_find(states^[:], path); existing != nil { return existing } data, err := os.read_entire_file(path, context.allocator) if err != nil || len(data) == 0 { if err == nil do delete(data) return nil } append(states, Rename_File_State{ path = strings.clone(path), original_storage = data, buffer = buffer_make(string(data)), }) return &states^[len(states^) - 1] } rename_file_state_find :: proc(states: []Rename_File_State, path: string) -> ^Rename_File_State { for &state in states { if state.path == path do return &state } return nil } rename_file_states_destroy :: proc(states: []Rename_File_State) { for &state in states { delete(state.path) buffer_destroy(&state.buffer) delete(state.original_storage) } } editor_buffer_for_rename_edit :: proc(editor: ^Editor, path: string) -> ^Editor_Buffer { for &buffer in editor.buffers { if buffer.path == path || buffer.daemon_path == path { return &buffer } } return nil } rename_panel_set_summary :: proc(panel: ^Rename_Panel, message: string) { delete(panel.summary) panel.summary = strings.clone(message) } parse_rename_response :: proc(response: string, expected_id: int) -> (string, [dynamic]Rename_Edit, bool) { edits: [dynamic]Rename_Edit 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") old_text, _ := json_get_string(item, "oldText") new_text, _ := json_get_string(item, "newText") _, file := os.split_path(path) label := fmt.tprintf("%s:%d:%d", file, line, column) append(&edits, Rename_Edit{ path = strings.clone(path), line = line, column = column, old_text = strings.clone(old_text), new_text = strings.clone(new_text), label = strings.clone(label), }) } } summary := fmt.tprintf("Preview: %s -> %s (%d edits). Press Enter again to apply.", old_name, new_name, len(edits)) return strings.clone(summary), edits, true } rename_edit_clone :: proc(edit: Rename_Edit) -> Rename_Edit { return Rename_Edit{ path = strings.clone(edit.path), line = edit.line, column = edit.column, old_text = strings.clone(edit.old_text), new_text = strings.clone(edit.new_text), label = strings.clone(edit.label), } } rename_edits_destroy :: proc(edits: []Rename_Edit) { for edit in edits { delete(edit.path) delete(edit.old_text) delete(edit.new_text) delete(edit.label) } }