From 59d102315d52e85c7df035d5784a0256db97fa4c Mon Sep 17 00:00:00 2001 From: "R.Eugenio" Date: Mon, 22 Dec 2025 13:16:59 +0100 Subject: [PATCH] fix(memory): Deep clone en Row + CellValue para evitar dangling pointers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CellValue.clone(): duplica strings en lugar de copiar punteros - CellValue.deinit(): libera strings clonados - Row.owns_data: flag para indicar propiedad de memoria - Row.clone(): ahora hace deep copy completo - Row.deinit(): libera strings si owns_data=true Soluciona crash al guardar cliente sin población/provincia/país, donde loadWhoList() liberaba strings que Row clonada seguía referenciando. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/widgets/advanced_table/types.zig | 42 ++++++++++++++++++++- src/widgets/autocomplete.zig | 56 ++++++++++++++++++++++++++-- 2 files changed, 92 insertions(+), 6 deletions(-) diff --git a/src/widgets/advanced_table/types.zig b/src/widgets/advanced_table/types.zig index c5f1c2b..095405b 100644 --- a/src/widgets/advanced_table/types.zig +++ b/src/widgets/advanced_table/types.zig @@ -158,6 +158,22 @@ pub const CellValue = union(enum) { .boolean => |b| if (b) "Yes" else "No", }; } + + /// Clone value with deep copy of strings + pub fn clone(self: CellValue, allocator: std.mem.Allocator) !CellValue { + return switch (self) { + .string => |s| .{ .string = if (s.len > 0) try allocator.dupe(u8, s) else "" }, + else => self, // Other types are value types, no allocation needed + }; + } + + /// Free allocated string (only call for cloned values) + pub fn deinit(self: CellValue, allocator: std.mem.Allocator) void { + switch (self) { + .string => |s| if (s.len > 0) allocator.free(s), + else => {}, + } + } }; // ============================================================================= @@ -452,15 +468,26 @@ pub const OnActiveRowChangedFn = *const fn (old_row: ?usize, new_row: usize, row pub const Row = struct { data: std.StringHashMap(CellValue), allocator: std.mem.Allocator, + /// True if this Row owns the string data (was created via clone) + /// When true, deinit() will free the strings + owns_data: bool = false, pub fn init(allocator: std.mem.Allocator) Row { return .{ .data = std.StringHashMap(CellValue).init(allocator), .allocator = allocator, + .owns_data = false, }; } pub fn deinit(self: *Row) void { + // Free cloned string values if we own them + if (self.owns_data) { + var iter = self.data.iterator(); + while (iter.next()) |entry| { + entry.value_ptr.*.deinit(self.allocator); + } + } self.data.deinit(); } @@ -472,11 +499,22 @@ pub const Row = struct { try self.data.put(column, value); } + /// Deep clone: duplicates all string values + /// The cloned Row owns its data and will free it on deinit() pub fn clone(self: *const Row, allocator: std.mem.Allocator) !Row { - var new_row = Row.init(allocator); + var new_row = Row{ + .data = std.StringHashMap(CellValue).init(allocator), + .allocator = allocator, + .owns_data = true, // Cloned row owns its string data + }; + errdefer new_row.deinit(); + var iter = self.data.iterator(); while (iter.next()) |entry| { - try new_row.data.put(entry.key_ptr.*, entry.value_ptr.*); + // Keys are typically string literals, no need to clone + // Values need deep clone for strings + const cloned_value = try entry.value_ptr.*.clone(allocator); + try new_row.data.put(entry.key_ptr.*, cloned_value); } return new_row; } diff --git a/src/widgets/autocomplete.zig b/src/widgets/autocomplete.zig index af1a71c..0aafc92 100644 --- a/src/widgets/autocomplete.zig +++ b/src/widgets/autocomplete.zig @@ -40,6 +40,10 @@ pub const AutoCompleteState = struct { was_focused: bool = false, /// First frame flag to avoid false focus detection first_frame: bool = true, + /// Texto original guardado al abrir dropdown (para restaurar con Escape) + saved_text: [256]u8 = [_]u8{0} ** 256, + saved_text_len: usize = 0, + saved_cursor: usize = 0, const Self = @This(); @@ -164,8 +168,29 @@ pub const AutoCompleteState = struct { } } - /// Open the dropdown + /// Guarda el texto actual (para restaurar con Escape) + pub fn saveText(self: *Self) void { + @memcpy(self.saved_text[0..self.len], self.buffer[0..self.len]); + self.saved_text_len = self.len; + self.saved_cursor = self.cursor; + } + + /// Restaura el texto guardado + pub fn restoreText(self: *Self) void { + @memcpy(self.buffer[0..self.saved_text_len], self.saved_text[0..self.saved_text_len]); + self.len = self.saved_text_len; + self.cursor = self.saved_cursor; + // Sync filter + @memcpy(self.last_filter[0..self.saved_text_len], self.saved_text[0..self.saved_text_len]); + self.last_filter_len = self.saved_text_len; + } + + /// Open the dropdown (guarda texto para posible restauración con Escape) pub fn openDropdown(self: *Self) void { + if (!self.open) { + // Solo guardar si estamos abriendo (no si ya está abierto) + self.saveText(); + } self.open = true; self.highlighted = if (self.selected >= 0) self.selected else 0; } @@ -175,6 +200,12 @@ pub const AutoCompleteState = struct { self.open = false; self.highlighted = -1; } + + /// Close dropdown y restaurar texto original (para Escape) + pub fn closeDropdownAndRestore(self: *Self) void { + self.restoreText(); + self.closeDropdown(); + } }; // ============================================================================= @@ -460,7 +491,10 @@ pub fn autocompleteRect( switch (event.key) { .escape => { - state.closeDropdown(); + // Escape: Cierra dropdown y RESTAURA texto original + if (state.open) { + state.closeDropdownAndRestore(); + } }, .enter => { if (state.open and state.highlighted >= 0 and state.highlighted < @as(i32, @intCast(filtered_count))) { @@ -480,7 +514,13 @@ pub fn autocompleteRect( } }, .up => { - if (state.open) { + // Ctrl+Up: Cierra dropdown (sin restaurar - confirma el estado actual) + if (ctx.input.modifiers.ctrl) { + if (state.open) { + state.closeDropdown(); + } + } else if (state.open) { + // Navegar hacia arriba en la lista if (state.highlighted > 0) { state.highlighted -= 1; // Scroll if needed @@ -489,11 +529,18 @@ pub fn autocompleteRect( } } } else { + // Abrir dropdown si está cerrado state.openDropdown(); } }, .down => { - if (state.open) { + // Ctrl+Down: Forzar apertura dropdown (SIN limpiar texto) + if (ctx.input.modifiers.ctrl) { + state.highlighted = 0; + state.scroll_offset = 0; + state.openDropdown(); + } else if (state.open) { + // Navegar hacia abajo en la lista if (state.highlighted < @as(i32, @intCast(filtered_count)) - 1) { state.highlighted += 1; // Scroll if needed @@ -503,6 +550,7 @@ pub fn autocompleteRect( } } } else { + // Abrir dropdown si está cerrado state.openDropdown(); } },