fix(memory): Deep clone en Row + CellValue para evitar dangling pointers
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
3f442bf8b9
commit
59d102315d
2 changed files with 92 additions and 6 deletions
|
|
@ -158,6 +158,22 @@ pub const CellValue = union(enum) {
|
||||||
.boolean => |b| if (b) "Yes" else "No",
|
.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 {
|
pub const Row = struct {
|
||||||
data: std.StringHashMap(CellValue),
|
data: std.StringHashMap(CellValue),
|
||||||
allocator: std.mem.Allocator,
|
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 {
|
pub fn init(allocator: std.mem.Allocator) Row {
|
||||||
return .{
|
return .{
|
||||||
.data = std.StringHashMap(CellValue).init(allocator),
|
.data = std.StringHashMap(CellValue).init(allocator),
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
|
.owns_data = false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Row) void {
|
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();
|
self.data.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -472,11 +499,22 @@ pub const Row = struct {
|
||||||
try self.data.put(column, value);
|
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 {
|
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();
|
var iter = self.data.iterator();
|
||||||
while (iter.next()) |entry| {
|
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;
|
return new_row;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,10 @@ pub const AutoCompleteState = struct {
|
||||||
was_focused: bool = false,
|
was_focused: bool = false,
|
||||||
/// First frame flag to avoid false focus detection
|
/// First frame flag to avoid false focus detection
|
||||||
first_frame: bool = true,
|
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();
|
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 {
|
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.open = true;
|
||||||
self.highlighted = if (self.selected >= 0) self.selected else 0;
|
self.highlighted = if (self.selected >= 0) self.selected else 0;
|
||||||
}
|
}
|
||||||
|
|
@ -175,6 +200,12 @@ pub const AutoCompleteState = struct {
|
||||||
self.open = false;
|
self.open = false;
|
||||||
self.highlighted = -1;
|
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) {
|
switch (event.key) {
|
||||||
.escape => {
|
.escape => {
|
||||||
state.closeDropdown();
|
// Escape: Cierra dropdown y RESTAURA texto original
|
||||||
|
if (state.open) {
|
||||||
|
state.closeDropdownAndRestore();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
.enter => {
|
.enter => {
|
||||||
if (state.open and state.highlighted >= 0 and state.highlighted < @as(i32, @intCast(filtered_count))) {
|
if (state.open and state.highlighted >= 0 and state.highlighted < @as(i32, @intCast(filtered_count))) {
|
||||||
|
|
@ -480,7 +514,13 @@ pub fn autocompleteRect(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
.up => {
|
.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) {
|
if (state.highlighted > 0) {
|
||||||
state.highlighted -= 1;
|
state.highlighted -= 1;
|
||||||
// Scroll if needed
|
// Scroll if needed
|
||||||
|
|
@ -489,11 +529,18 @@ pub fn autocompleteRect(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Abrir dropdown si está cerrado
|
||||||
state.openDropdown();
|
state.openDropdown();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
.down => {
|
.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) {
|
if (state.highlighted < @as(i32, @intCast(filtered_count)) - 1) {
|
||||||
state.highlighted += 1;
|
state.highlighted += 1;
|
||||||
// Scroll if needed
|
// Scroll if needed
|
||||||
|
|
@ -503,6 +550,7 @@ pub fn autocompleteRect(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Abrir dropdown si está cerrado
|
||||||
state.openDropdown();
|
state.openDropdown();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue