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

@ -0,0 +1,283 @@
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)
}
}