283 lines
9.3 KiB
Odin
283 lines
9.3 KiB
Odin
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)
|
|
}
|
|
}
|