mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-19 05:50:27 +00:00
Binding internals improvements (#10243)
A handful of improvements. See individual commits. 1. **Actually compare values for the binding set.** This sounds crazy, but up until now (for _years_) we've only compared _the hash value_ of a trigger or action for our binding set. It's actually astounding this hasn't bit us or at least not that we know of. This could result in different triggers overwriting each other. Anyways, we actually compare them now. 2. **Use an `ArrayHashMap` for sets.** This has been on the back burner for awhile. Using an array hash map is a good idea in general (see: https://github.com/ziglang/zig/issues/17851) but it also is a nicer API for our use case and cleaned things up. All unit tests pass, many new unit tests added particularly for equality comparison. Hopeful this doesn't regress any bindings but this is the right path forward so we should fix those if they come up. **AI disclosure:** AI helped write the deepEqual unit tests, otherwise everything else is certified meat.
This commit is contained in:
@@ -17,6 +17,7 @@ const assert = @import("../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const global_state = &@import("../global.zig").state;
|
||||
const deepEqual = @import("../datastruct/comparison.zig").deepEqual;
|
||||
const fontpkg = @import("../font/main.zig");
|
||||
const inputpkg = @import("../input.zig");
|
||||
const internal_os = @import("../os/main.zig");
|
||||
@@ -4120,7 +4121,7 @@ pub fn changeConditionalState(
|
||||
|
||||
// Conditional set contains the keys that this config uses. So we
|
||||
// only continue if we use this key.
|
||||
if (self._conditional_set.contains(key) and !equalField(
|
||||
if (self._conditional_set.contains(key) and !deepEqual(
|
||||
@TypeOf(@field(self._conditional_state, field.name)),
|
||||
@field(self._conditional_state, field.name),
|
||||
@field(new, field.name),
|
||||
@@ -4826,7 +4827,7 @@ pub fn changed(self: *const Config, new: *const Config, comptime key: Key) bool
|
||||
|
||||
const old_value = @field(self, field.name);
|
||||
const new_value = @field(new, field.name);
|
||||
return !equalField(field.type, old_value, new_value);
|
||||
return !deepEqual(field.type, old_value, new_value);
|
||||
}
|
||||
|
||||
/// This yields a key for every changed field between old and new.
|
||||
@@ -4854,91 +4855,6 @@ pub const ChangeIterator = struct {
|
||||
}
|
||||
};
|
||||
|
||||
/// A config-specific helper to determine if two values of the same
|
||||
/// type are equal. This isn't the same as std.mem.eql or std.testing.equals
|
||||
/// because we expect structs to implement their own equality.
|
||||
///
|
||||
/// This also doesn't support ALL Zig types, because we only add to it
|
||||
/// as we need types for the config.
|
||||
fn equalField(comptime T: type, old: T, new: T) bool {
|
||||
// Do known named types first
|
||||
switch (T) {
|
||||
inline []const u8,
|
||||
[:0]const u8,
|
||||
=> return std.mem.eql(u8, old, new),
|
||||
|
||||
[]const [:0]const u8,
|
||||
=> {
|
||||
if (old.len != new.len) return false;
|
||||
for (old, new) |a, b| {
|
||||
if (!std.mem.eql(u8, a, b)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
else => {},
|
||||
}
|
||||
|
||||
// Back into types of types
|
||||
switch (@typeInfo(T)) {
|
||||
.void => return true,
|
||||
|
||||
inline .bool,
|
||||
.int,
|
||||
.float,
|
||||
.@"enum",
|
||||
=> return old == new,
|
||||
|
||||
.optional => |info| {
|
||||
if (old == null and new == null) return true;
|
||||
if (old == null or new == null) return false;
|
||||
return equalField(info.child, old.?, new.?);
|
||||
},
|
||||
|
||||
.@"struct" => |info| {
|
||||
if (@hasDecl(T, "equal")) return old.equal(new);
|
||||
|
||||
// If a struct doesn't declare an "equal" function, we fall back
|
||||
// to a recursive field-by-field compare.
|
||||
inline for (info.fields) |field_info| {
|
||||
if (!equalField(
|
||||
field_info.type,
|
||||
@field(old, field_info.name),
|
||||
@field(new, field_info.name),
|
||||
)) return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
.@"union" => |info| {
|
||||
if (@hasDecl(T, "equal")) return old.equal(new);
|
||||
|
||||
const tag_type = info.tag_type.?;
|
||||
const old_tag = std.meta.activeTag(old);
|
||||
const new_tag = std.meta.activeTag(new);
|
||||
if (old_tag != new_tag) return false;
|
||||
|
||||
inline for (info.fields) |field_info| {
|
||||
if (@field(tag_type, field_info.name) == old_tag) {
|
||||
return equalField(
|
||||
field_info.type,
|
||||
@field(old, field_info.name),
|
||||
@field(new, field_info.name),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
unreachable;
|
||||
},
|
||||
|
||||
else => {
|
||||
@compileLog(T);
|
||||
@compileError("unsupported field type");
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// This runs a heuristic to determine if we are likely running
|
||||
/// Ghostty in a CLI environment. We need this to change some behaviors.
|
||||
/// We should keep the set of behaviors that depend on this as small
|
||||
@@ -6885,7 +6801,7 @@ pub const Keybinds = struct {
|
||||
const self_leaf = self_entry.value_ptr.*.leaf;
|
||||
const other_leaf = other_entry.value_ptr.*.leaf;
|
||||
|
||||
if (!equalField(
|
||||
if (!deepEqual(
|
||||
inputpkg.Binding.Set.Leaf,
|
||||
self_leaf,
|
||||
other_leaf,
|
||||
@@ -6899,7 +6815,7 @@ pub const Keybinds = struct {
|
||||
if (self_chain.flags != other_chain.flags) return false;
|
||||
if (self_chain.actions.items.len != other_chain.actions.items.len) return false;
|
||||
for (self_chain.actions.items, other_chain.actions.items) |a1, a2| {
|
||||
if (!equalField(
|
||||
if (!deepEqual(
|
||||
inputpkg.Binding.Action,
|
||||
a1,
|
||||
a2,
|
||||
@@ -6914,7 +6830,7 @@ pub const Keybinds = struct {
|
||||
|
||||
/// Like formatEntry but has an option to include docs.
|
||||
pub fn formatEntryDocs(self: Keybinds, formatter: formatterpkg.EntryFormatter, docs: bool) !void {
|
||||
if (self.set.bindings.size == 0 and self.tables.count() == 0) {
|
||||
if (self.set.bindings.count() == 0 and self.tables.count() == 0) {
|
||||
try formatter.formatEntry(void, {});
|
||||
return;
|
||||
}
|
||||
@@ -7016,8 +6932,8 @@ pub const Keybinds = struct {
|
||||
// Note they turn into translated keys because they match
|
||||
// their ASCII mapping.
|
||||
const want =
|
||||
\\keybind = ctrl+z>2=goto_tab:2
|
||||
\\keybind = ctrl+z>1=goto_tab:1
|
||||
\\keybind = ctrl+z>2=goto_tab:2
|
||||
\\
|
||||
;
|
||||
try std.testing.expectEqualStrings(want, buf.written());
|
||||
@@ -7041,9 +6957,9 @@ pub const Keybinds = struct {
|
||||
|
||||
// NB: This does not currently retain the order of the keybinds.
|
||||
const want =
|
||||
\\a = ctrl+a>ctrl+c>t=new_tab
|
||||
\\a = ctrl+a>ctrl+b>w=close_window
|
||||
\\a = ctrl+a>ctrl+b>n=new_window
|
||||
\\a = ctrl+a>ctrl+b>w=close_window
|
||||
\\a = ctrl+a>ctrl+c>t=new_tab
|
||||
\\a = ctrl+b>ctrl+d>a=previous_tab
|
||||
\\
|
||||
;
|
||||
|
||||
@@ -1,11 +1,102 @@
|
||||
// The contents of this file is largely based on testing.zig from the Zig 0.15.1
|
||||
// stdlib, distributed under the MIT license, copyright (c) Zig contributors
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
|
||||
/// A deep equality comparison function that works for most types. We
|
||||
/// add types as necessary. It defers to `equal` decls on types that support
|
||||
/// decls.
|
||||
pub fn deepEqual(comptime T: type, old: T, new: T) bool {
|
||||
// Do known named types first
|
||||
switch (T) {
|
||||
inline []const u8,
|
||||
[:0]const u8,
|
||||
=> return std.mem.eql(u8, old, new),
|
||||
|
||||
[]const [:0]const u8,
|
||||
=> {
|
||||
if (old.len != new.len) return false;
|
||||
for (old, new) |a, b| {
|
||||
if (!std.mem.eql(u8, a, b)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
else => {},
|
||||
}
|
||||
|
||||
// Back into types of types
|
||||
switch (@typeInfo(T)) {
|
||||
.void => return true,
|
||||
|
||||
inline .bool,
|
||||
.int,
|
||||
.float,
|
||||
.@"enum",
|
||||
=> return old == new,
|
||||
|
||||
.optional => |info| {
|
||||
if (old == null and new == null) return true;
|
||||
if (old == null or new == null) return false;
|
||||
return deepEqual(info.child, old.?, new.?);
|
||||
},
|
||||
|
||||
.array => |info| for (old, new) |old_elem, new_elem| {
|
||||
if (!deepEqual(
|
||||
info.child,
|
||||
old_elem,
|
||||
new_elem,
|
||||
)) return false;
|
||||
} else return true,
|
||||
|
||||
.@"struct" => |info| {
|
||||
if (@hasDecl(T, "equal")) return old.equal(new);
|
||||
|
||||
// If a struct doesn't declare an "equal" function, we fall back
|
||||
// to a recursive field-by-field compare.
|
||||
inline for (info.fields) |field_info| {
|
||||
if (!deepEqual(
|
||||
field_info.type,
|
||||
@field(old, field_info.name),
|
||||
@field(new, field_info.name),
|
||||
)) return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
.@"union" => |info| {
|
||||
if (@hasDecl(T, "equal")) return old.equal(new);
|
||||
|
||||
const tag_type = info.tag_type.?;
|
||||
const old_tag = std.meta.activeTag(old);
|
||||
const new_tag = std.meta.activeTag(new);
|
||||
if (old_tag != new_tag) return false;
|
||||
|
||||
inline for (info.fields) |field_info| {
|
||||
if (@field(tag_type, field_info.name) == old_tag) {
|
||||
return deepEqual(
|
||||
field_info.type,
|
||||
@field(old, field_info.name),
|
||||
@field(new, field_info.name),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
unreachable;
|
||||
},
|
||||
|
||||
else => {
|
||||
@compileLog(T);
|
||||
@compileError("unsupported field type");
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic, recursive equality testing utility using approximate comparison for
|
||||
/// floats and equality for everything else
|
||||
///
|
||||
/// Based on `std.testing.expectEqual` and `std.testing.expectEqualSlices`.
|
||||
/// Based on `testing.expectEqual` and `testing.expectEqualSlices`.
|
||||
///
|
||||
/// The relative tolerance is currently hardcoded to `sqrt(eps(float_type))`.
|
||||
pub inline fn expectApproxEqual(expected: anytype, actual: anytype) !void {
|
||||
@@ -59,7 +150,7 @@ fn expectApproxEqualInner(comptime T: type, expected: T, actual: T) !void {
|
||||
if (union_info.tag_type == null) {
|
||||
// untagged unions can only be compared bitwise,
|
||||
// so expectEqual is all we need
|
||||
std.testing.expectEqual(expected, actual) catch {
|
||||
testing.expectEqual(expected, actual) catch {
|
||||
return error.TestExpectedApproxEqual;
|
||||
};
|
||||
}
|
||||
@@ -69,7 +160,7 @@ fn expectApproxEqualInner(comptime T: type, expected: T, actual: T) !void {
|
||||
const expectedTag = @as(Tag, expected);
|
||||
const actualTag = @as(Tag, actual);
|
||||
|
||||
std.testing.expectEqual(expectedTag, actualTag) catch {
|
||||
testing.expectEqual(expectedTag, actualTag) catch {
|
||||
return error.TestExpectedApproxEqual;
|
||||
};
|
||||
|
||||
@@ -84,23 +175,23 @@ fn expectApproxEqualInner(comptime T: type, expected: T, actual: T) !void {
|
||||
};
|
||||
// we only reach this point if there's at least one null or error,
|
||||
// in which case expectEqual is all we need
|
||||
std.testing.expectEqual(expected, actual) catch {
|
||||
testing.expectEqual(expected, actual) catch {
|
||||
return error.TestExpectedApproxEqual;
|
||||
};
|
||||
},
|
||||
|
||||
// fall back to expectEqual for everything else
|
||||
else => std.testing.expectEqual(expected, actual) catch {
|
||||
else => testing.expectEqual(expected, actual) catch {
|
||||
return error.TestExpectedApproxEqual;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy of std.testing.print (not public)
|
||||
/// Copy of testing.print (not public)
|
||||
fn print(comptime fmt: []const u8, args: anytype) void {
|
||||
if (@inComptime()) {
|
||||
@compileError(std.fmt.comptimePrint(fmt, args));
|
||||
} else if (std.testing.backend_can_print) {
|
||||
} else if (testing.backend_can_print) {
|
||||
std.debug.print(fmt, args);
|
||||
}
|
||||
}
|
||||
@@ -145,3 +236,195 @@ test "expectApproxEqual struct" {
|
||||
|
||||
try expectApproxEqual(a, b);
|
||||
}
|
||||
|
||||
test "deepEqual void" {
|
||||
try testing.expect(deepEqual(void, {}, {}));
|
||||
}
|
||||
|
||||
test "deepEqual bool" {
|
||||
try testing.expect(deepEqual(bool, true, true));
|
||||
try testing.expect(deepEqual(bool, false, false));
|
||||
try testing.expect(!deepEqual(bool, true, false));
|
||||
try testing.expect(!deepEqual(bool, false, true));
|
||||
}
|
||||
|
||||
test "deepEqual int" {
|
||||
try testing.expect(deepEqual(i32, 42, 42));
|
||||
try testing.expect(deepEqual(i32, -100, -100));
|
||||
try testing.expect(!deepEqual(i32, 42, 43));
|
||||
try testing.expect(deepEqual(u64, 0, 0));
|
||||
try testing.expect(!deepEqual(u64, 0, 1));
|
||||
}
|
||||
|
||||
test "deepEqual float" {
|
||||
try testing.expect(deepEqual(f32, 1.0, 1.0));
|
||||
try testing.expect(!deepEqual(f32, 1.0, 1.1));
|
||||
try testing.expect(deepEqual(f64, 3.14159, 3.14159));
|
||||
try testing.expect(!deepEqual(f64, 3.14159, 3.14158));
|
||||
}
|
||||
|
||||
test "deepEqual enum" {
|
||||
const Color = enum { red, green, blue };
|
||||
try testing.expect(deepEqual(Color, .red, .red));
|
||||
try testing.expect(deepEqual(Color, .blue, .blue));
|
||||
try testing.expect(!deepEqual(Color, .red, .green));
|
||||
try testing.expect(!deepEqual(Color, .green, .blue));
|
||||
}
|
||||
|
||||
test "deepEqual []const u8" {
|
||||
try testing.expect(deepEqual([]const u8, "hello", "hello"));
|
||||
try testing.expect(deepEqual([]const u8, "", ""));
|
||||
try testing.expect(!deepEqual([]const u8, "hello", "world"));
|
||||
try testing.expect(!deepEqual([]const u8, "hello", "hell"));
|
||||
try testing.expect(!deepEqual([]const u8, "hello", "hello!"));
|
||||
}
|
||||
|
||||
test "deepEqual [:0]const u8" {
|
||||
try testing.expect(deepEqual([:0]const u8, "foo", "foo"));
|
||||
try testing.expect(!deepEqual([:0]const u8, "foo", "bar"));
|
||||
try testing.expect(!deepEqual([:0]const u8, "foo", "fo"));
|
||||
}
|
||||
|
||||
test "deepEqual []const [:0]const u8" {
|
||||
const a: []const [:0]const u8 = &.{ "one", "two", "three" };
|
||||
const b: []const [:0]const u8 = &.{ "one", "two", "three" };
|
||||
const c: []const [:0]const u8 = &.{ "one", "two" };
|
||||
const d: []const [:0]const u8 = &.{ "one", "two", "four" };
|
||||
const e: []const [:0]const u8 = &.{};
|
||||
|
||||
try testing.expect(deepEqual([]const [:0]const u8, a, b));
|
||||
try testing.expect(!deepEqual([]const [:0]const u8, a, c));
|
||||
try testing.expect(!deepEqual([]const [:0]const u8, a, d));
|
||||
try testing.expect(deepEqual([]const [:0]const u8, e, e));
|
||||
try testing.expect(!deepEqual([]const [:0]const u8, a, e));
|
||||
}
|
||||
|
||||
test "deepEqual optional" {
|
||||
try testing.expect(deepEqual(?i32, null, null));
|
||||
try testing.expect(deepEqual(?i32, 42, 42));
|
||||
try testing.expect(!deepEqual(?i32, null, 42));
|
||||
try testing.expect(!deepEqual(?i32, 42, null));
|
||||
try testing.expect(!deepEqual(?i32, 42, 43));
|
||||
}
|
||||
|
||||
test "deepEqual optional nested" {
|
||||
const Nested = struct { x: i32, y: i32 };
|
||||
try testing.expect(deepEqual(?Nested, null, null));
|
||||
try testing.expect(deepEqual(?Nested, .{ .x = 1, .y = 2 }, .{ .x = 1, .y = 2 }));
|
||||
try testing.expect(!deepEqual(?Nested, .{ .x = 1, .y = 2 }, .{ .x = 1, .y = 3 }));
|
||||
try testing.expect(!deepEqual(?Nested, .{ .x = 1, .y = 2 }, null));
|
||||
}
|
||||
|
||||
test "deepEqual array" {
|
||||
try testing.expect(deepEqual([3]i32, .{ 1, 2, 3 }, .{ 1, 2, 3 }));
|
||||
try testing.expect(!deepEqual([3]i32, .{ 1, 2, 3 }, .{ 1, 2, 4 }));
|
||||
try testing.expect(!deepEqual([3]i32, .{ 1, 2, 3 }, .{ 0, 2, 3 }));
|
||||
try testing.expect(deepEqual([0]i32, .{}, .{}));
|
||||
}
|
||||
|
||||
test "deepEqual nested array" {
|
||||
const a = [2][2]i32{ .{ 1, 2 }, .{ 3, 4 } };
|
||||
const b = [2][2]i32{ .{ 1, 2 }, .{ 3, 4 } };
|
||||
const c = [2][2]i32{ .{ 1, 2 }, .{ 3, 5 } };
|
||||
|
||||
try testing.expect(deepEqual([2][2]i32, a, b));
|
||||
try testing.expect(!deepEqual([2][2]i32, a, c));
|
||||
}
|
||||
|
||||
test "deepEqual struct" {
|
||||
const Point = struct { x: i32, y: i32 };
|
||||
try testing.expect(deepEqual(Point, .{ .x = 10, .y = 20 }, .{ .x = 10, .y = 20 }));
|
||||
try testing.expect(!deepEqual(Point, .{ .x = 10, .y = 20 }, .{ .x = 10, .y = 21 }));
|
||||
try testing.expect(!deepEqual(Point, .{ .x = 10, .y = 20 }, .{ .x = 11, .y = 20 }));
|
||||
}
|
||||
|
||||
test "deepEqual struct nested" {
|
||||
const Inner = struct { value: i32 };
|
||||
const Outer = struct { a: Inner, b: Inner };
|
||||
|
||||
const x = Outer{ .a = .{ .value = 1 }, .b = .{ .value = 2 } };
|
||||
const y = Outer{ .a = .{ .value = 1 }, .b = .{ .value = 2 } };
|
||||
const z = Outer{ .a = .{ .value = 1 }, .b = .{ .value = 3 } };
|
||||
|
||||
try testing.expect(deepEqual(Outer, x, y));
|
||||
try testing.expect(!deepEqual(Outer, x, z));
|
||||
}
|
||||
|
||||
test "deepEqual struct with equal decl" {
|
||||
const Custom = struct {
|
||||
value: i32,
|
||||
|
||||
pub fn equal(self: @This(), other: @This()) bool {
|
||||
return @mod(self.value, 10) == @mod(other.value, 10);
|
||||
}
|
||||
};
|
||||
|
||||
try testing.expect(deepEqual(Custom, .{ .value = 5 }, .{ .value = 15 }));
|
||||
try testing.expect(deepEqual(Custom, .{ .value = 100 }, .{ .value = 0 }));
|
||||
try testing.expect(!deepEqual(Custom, .{ .value = 5 }, .{ .value = 6 }));
|
||||
}
|
||||
|
||||
test "deepEqual union" {
|
||||
const Value = union(enum) {
|
||||
int: i32,
|
||||
float: f32,
|
||||
none,
|
||||
};
|
||||
|
||||
try testing.expect(deepEqual(Value, .{ .int = 42 }, .{ .int = 42 }));
|
||||
try testing.expect(!deepEqual(Value, .{ .int = 42 }, .{ .int = 43 }));
|
||||
try testing.expect(!deepEqual(Value, .{ .int = 42 }, .{ .float = 42.0 }));
|
||||
try testing.expect(deepEqual(Value, .none, .none));
|
||||
try testing.expect(!deepEqual(Value, .none, .{ .int = 0 }));
|
||||
}
|
||||
|
||||
test "deepEqual union with equal decl" {
|
||||
const Value = union(enum) {
|
||||
num: i32,
|
||||
str: []const u8,
|
||||
|
||||
pub fn equal(self: @This(), other: @This()) bool {
|
||||
return switch (self) {
|
||||
.num => |n| switch (other) {
|
||||
.num => |m| @mod(n, 10) == @mod(m, 10),
|
||||
else => false,
|
||||
},
|
||||
.str => |s| switch (other) {
|
||||
.str => |t| s.len == t.len,
|
||||
else => false,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
try testing.expect(deepEqual(Value, .{ .num = 5 }, .{ .num = 25 }));
|
||||
try testing.expect(!deepEqual(Value, .{ .num = 5 }, .{ .num = 6 }));
|
||||
try testing.expect(deepEqual(Value, .{ .str = "abc" }, .{ .str = "xyz" }));
|
||||
try testing.expect(!deepEqual(Value, .{ .str = "abc" }, .{ .str = "ab" }));
|
||||
}
|
||||
|
||||
test "deepEqual array of structs" {
|
||||
const Item = struct { id: i32, name: []const u8 };
|
||||
const a = [2]Item{ .{ .id = 1, .name = "one" }, .{ .id = 2, .name = "two" } };
|
||||
const b = [2]Item{ .{ .id = 1, .name = "one" }, .{ .id = 2, .name = "two" } };
|
||||
const c = [2]Item{ .{ .id = 1, .name = "one" }, .{ .id = 2, .name = "TWO" } };
|
||||
|
||||
try testing.expect(deepEqual([2]Item, a, b));
|
||||
try testing.expect(!deepEqual([2]Item, a, c));
|
||||
}
|
||||
|
||||
test "deepEqual struct with optional field" {
|
||||
const Config = struct { name: []const u8, port: ?u16 };
|
||||
|
||||
try testing.expect(deepEqual(Config, .{ .name = "app", .port = 8080 }, .{ .name = "app", .port = 8080 }));
|
||||
try testing.expect(deepEqual(Config, .{ .name = "app", .port = null }, .{ .name = "app", .port = null }));
|
||||
try testing.expect(!deepEqual(Config, .{ .name = "app", .port = 8080 }, .{ .name = "app", .port = null }));
|
||||
try testing.expect(!deepEqual(Config, .{ .name = "app", .port = 8080 }, .{ .name = "app", .port = 8081 }));
|
||||
}
|
||||
|
||||
test "deepEqual struct with array field" {
|
||||
const Data = struct { values: [3]i32 };
|
||||
|
||||
try testing.expect(deepEqual(Data, .{ .values = .{ 1, 2, 3 } }, .{ .values = .{ 1, 2, 3 } }));
|
||||
try testing.expect(!deepEqual(Data, .{ .values = .{ 1, 2, 3 } }, .{ .values = .{ 1, 2, 4 } }));
|
||||
}
|
||||
|
||||
@@ -19,4 +19,6 @@ pub const SplitTree = split_tree.SplitTree;
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
|
||||
_ = @import("comparison.zig");
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ const assert = @import("../quirks.zig").inlineAssert;
|
||||
const build_config = @import("../build_config.zig");
|
||||
const uucode = @import("uucode");
|
||||
const EntryFormatter = @import("../config/formatter.zig").EntryFormatter;
|
||||
const deepEqual = @import("../datastruct/comparison.zig").deepEqual;
|
||||
const key = @import("key.zig");
|
||||
const key_mods = @import("key_mods.zig");
|
||||
const KeyEvent = key.KeyEvent;
|
||||
@@ -1587,6 +1588,24 @@ pub const Action = union(enum) {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Compares two actions for equality.
|
||||
pub fn equal(self: Action, other: Action) bool {
|
||||
if (std.meta.activeTag(self) != std.meta.activeTag(other)) return false;
|
||||
return switch (self) {
|
||||
inline else => |field_self, tag| {
|
||||
const field_other = @field(other, @tagName(tag));
|
||||
return deepEqual(
|
||||
@TypeOf(field_self),
|
||||
field_self,
|
||||
field_other,
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// For the Set.Context
|
||||
const bindingSetEqual = equal;
|
||||
};
|
||||
|
||||
/// Trigger is the associated key state that can trigger an action.
|
||||
@@ -1908,7 +1927,7 @@ pub const Trigger = struct {
|
||||
}
|
||||
|
||||
/// Returns true if two triggers are equal.
|
||||
pub fn eql(self: Trigger, other: Trigger) bool {
|
||||
pub fn equal(self: Trigger, other: Trigger) bool {
|
||||
if (self.mods != other.mods) return false;
|
||||
const self_tag = std.meta.activeTag(self.key);
|
||||
const other_tag = std.meta.activeTag(other.key);
|
||||
@@ -1920,6 +1939,26 @@ pub const Trigger = struct {
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns true if two triggers are equal using folded codepoints.
|
||||
pub fn foldedEqual(self: Trigger, other: Trigger) bool {
|
||||
if (self.mods != other.mods) return false;
|
||||
const self_tag = std.meta.activeTag(self.key);
|
||||
const other_tag = std.meta.activeTag(other.key);
|
||||
if (self_tag != other_tag) return false;
|
||||
return switch (self.key) {
|
||||
.physical => |v| v == other.key.physical,
|
||||
.unicode => |v| deepEqual(
|
||||
[3]u21,
|
||||
foldedCodepoint(v),
|
||||
foldedCodepoint(other.key.unicode),
|
||||
),
|
||||
.catch_all => true,
|
||||
};
|
||||
}
|
||||
|
||||
/// For the Set.Context
|
||||
const bindingSetEqual = foldedEqual;
|
||||
|
||||
/// Convert the trigger to a C API compatible trigger.
|
||||
pub fn cval(self: Trigger) C {
|
||||
return .{
|
||||
@@ -1959,18 +1998,18 @@ pub const Trigger = struct {
|
||||
/// The use case is that this will be called on EVERY key input to look
|
||||
/// for an associated action so it must be fast.
|
||||
pub const Set = struct {
|
||||
const HashMap = std.HashMapUnmanaged(
|
||||
const HashMap = std.ArrayHashMapUnmanaged(
|
||||
Trigger,
|
||||
Value,
|
||||
Context(Trigger),
|
||||
std.hash_map.default_max_load_percentage,
|
||||
true,
|
||||
);
|
||||
|
||||
const ReverseMap = std.HashMapUnmanaged(
|
||||
const ReverseMap = std.ArrayHashMapUnmanaged(
|
||||
Action,
|
||||
Trigger,
|
||||
Context(Action),
|
||||
std.hash_map.default_max_load_percentage,
|
||||
true,
|
||||
);
|
||||
|
||||
/// The set of bindings.
|
||||
@@ -2464,11 +2503,10 @@ pub const Set = struct {
|
||||
// update the reverse mapping to remove the old action.
|
||||
.leaf => if (track_reverse) {
|
||||
const t_hash = t.hash();
|
||||
var it = self.reverse.iterator();
|
||||
while (it.next()) |reverse_entry| it: {
|
||||
if (t_hash == reverse_entry.value_ptr.hash()) {
|
||||
self.reverse.removeByPtr(reverse_entry.key_ptr);
|
||||
break :it;
|
||||
for (0.., self.reverse.values()) |i, *value| {
|
||||
if (t_hash == value.hash()) {
|
||||
self.reverse.swapRemoveAt(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2484,7 +2522,7 @@ pub const Set = struct {
|
||||
.action = action,
|
||||
.flags = flags,
|
||||
} };
|
||||
errdefer _ = self.bindings.remove(t);
|
||||
errdefer _ = self.bindings.swapRemove(t);
|
||||
|
||||
if (track_reverse) try self.reverse.put(alloc, action, t);
|
||||
errdefer if (track_reverse) self.reverse.remove(action);
|
||||
@@ -2624,7 +2662,7 @@ pub const Set = struct {
|
||||
self.chain_parent = null;
|
||||
|
||||
var entry = self.bindings.get(t) orelse return;
|
||||
_ = self.bindings.remove(t);
|
||||
_ = self.bindings.swapRemove(t);
|
||||
|
||||
switch (entry) {
|
||||
// For a leader removal, we need to deallocate our child set.
|
||||
@@ -2674,7 +2712,7 @@ pub const Set = struct {
|
||||
|
||||
// If our value is not the same as the old trigger, we can
|
||||
// ignore it because our reverse mapping points somewhere else.
|
||||
if (!entry.value_ptr.eql(old)) return;
|
||||
if (!entry.value_ptr.equal(old)) return;
|
||||
|
||||
// It is the same trigger, so let's now go through our bindings
|
||||
// and try to find another trigger that maps to the same action.
|
||||
@@ -2694,7 +2732,7 @@ pub const Set = struct {
|
||||
|
||||
// No other trigger points to this action so we remove
|
||||
// the reverse mapping completely.
|
||||
_ = self.reverse.remove(action);
|
||||
_ = self.reverse.swapRemove(action);
|
||||
}
|
||||
|
||||
/// Deep clone the set.
|
||||
@@ -2727,9 +2765,8 @@ pub const Set = struct {
|
||||
|
||||
// We need to clone the action keys in the reverse map since
|
||||
// they may contain allocated values.
|
||||
{
|
||||
var it = result.reverse.keyIterator();
|
||||
while (it.next()) |action| action.* = try action.clone(alloc);
|
||||
for (result.reverse.keys()) |*action| {
|
||||
action.* = try action.clone(alloc);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -2739,13 +2776,23 @@ pub const Set = struct {
|
||||
/// gets the hash key and checks for equality.
|
||||
fn Context(comptime KeyType: type) type {
|
||||
return struct {
|
||||
pub fn hash(ctx: @This(), k: KeyType) u64 {
|
||||
pub fn hash(ctx: @This(), k: KeyType) u32 {
|
||||
_ = ctx;
|
||||
return k.hash();
|
||||
// This seems crazy at first glance but this is also how
|
||||
// the Zig standard library handles hashing for array
|
||||
// hash maps!
|
||||
return @truncate(k.hash());
|
||||
}
|
||||
|
||||
pub fn eql(ctx: @This(), a: KeyType, b: KeyType) bool {
|
||||
return ctx.hash(a) == ctx.hash(b);
|
||||
pub fn eql(
|
||||
ctx: @This(),
|
||||
a: KeyType,
|
||||
b: KeyType,
|
||||
b_index: usize,
|
||||
) bool {
|
||||
_ = ctx;
|
||||
_ = b_index;
|
||||
return a.bindingSetEqual(b);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -3110,63 +3157,63 @@ test "parse: all triggers" {
|
||||
}
|
||||
}
|
||||
|
||||
test "Trigger: eql" {
|
||||
test "Trigger: equal" {
|
||||
const testing = std.testing;
|
||||
|
||||
// Equal physical keys
|
||||
{
|
||||
const t1: Trigger = .{ .key = .{ .physical = .arrow_up }, .mods = .{ .ctrl = true } };
|
||||
const t2: Trigger = .{ .key = .{ .physical = .arrow_up }, .mods = .{ .ctrl = true } };
|
||||
try testing.expect(t1.eql(t2));
|
||||
try testing.expect(t1.equal(t2));
|
||||
}
|
||||
|
||||
// Different physical keys
|
||||
{
|
||||
const t1: Trigger = .{ .key = .{ .physical = .arrow_up }, .mods = .{ .ctrl = true } };
|
||||
const t2: Trigger = .{ .key = .{ .physical = .arrow_down }, .mods = .{ .ctrl = true } };
|
||||
try testing.expect(!t1.eql(t2));
|
||||
try testing.expect(!t1.equal(t2));
|
||||
}
|
||||
|
||||
// Different mods
|
||||
{
|
||||
const t1: Trigger = .{ .key = .{ .physical = .arrow_up }, .mods = .{ .ctrl = true } };
|
||||
const t2: Trigger = .{ .key = .{ .physical = .arrow_up }, .mods = .{ .shift = true } };
|
||||
try testing.expect(!t1.eql(t2));
|
||||
try testing.expect(!t1.equal(t2));
|
||||
}
|
||||
|
||||
// Equal unicode keys
|
||||
{
|
||||
const t1: Trigger = .{ .key = .{ .unicode = 'a' }, .mods = .{} };
|
||||
const t2: Trigger = .{ .key = .{ .unicode = 'a' }, .mods = .{} };
|
||||
try testing.expect(t1.eql(t2));
|
||||
try testing.expect(t1.equal(t2));
|
||||
}
|
||||
|
||||
// Different unicode keys
|
||||
{
|
||||
const t1: Trigger = .{ .key = .{ .unicode = 'a' }, .mods = .{} };
|
||||
const t2: Trigger = .{ .key = .{ .unicode = 'b' }, .mods = .{} };
|
||||
try testing.expect(!t1.eql(t2));
|
||||
try testing.expect(!t1.equal(t2));
|
||||
}
|
||||
|
||||
// Different key types
|
||||
{
|
||||
const t1: Trigger = .{ .key = .{ .unicode = 'a' }, .mods = .{} };
|
||||
const t2: Trigger = .{ .key = .{ .physical = .key_a }, .mods = .{} };
|
||||
try testing.expect(!t1.eql(t2));
|
||||
try testing.expect(!t1.equal(t2));
|
||||
}
|
||||
|
||||
// catch_all
|
||||
{
|
||||
const t1: Trigger = .{ .key = .catch_all, .mods = .{} };
|
||||
const t2: Trigger = .{ .key = .catch_all, .mods = .{} };
|
||||
try testing.expect(t1.eql(t2));
|
||||
try testing.expect(t1.equal(t2));
|
||||
}
|
||||
|
||||
// catch_all with different mods
|
||||
{
|
||||
const t1: Trigger = .{ .key = .catch_all, .mods = .{} };
|
||||
const t2: Trigger = .{ .key = .catch_all, .mods = .{ .alt = true } };
|
||||
try testing.expect(!t1.eql(t2));
|
||||
try testing.expect(!t1.equal(t2));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user