diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/dump.zig | 2 | ||||
| -rw-r--r-- | src/locations.zig | 292 | ||||
| -rw-r--r-- | src/locations_cache.zig | 213 | ||||
| -rw-r--r-- | src/locations_config.zig | 75 | ||||
| -rw-r--r-- | src/main.zig | 2 | ||||
| -rw-r--r-- | src/pardes.zig | 8 |
6 files changed, 296 insertions, 296 deletions
diff --git a/src/dump.zig b/src/dump.zig index a2efb95d..1eabdae8 100644 --- a/src/dump.zig +++ b/src/dump.zig @@ -278,7 +278,7 @@ pub const State = struct { topbar: []const u8 = "", topbar_custom: ?[]const u8 = null, theme: []const u8 = "dark", - locations_config: @import("locations_config.zig").Config = .{}, + locations_config: @import("locations.zig").Config = .{}, tree_context_tag_style: bool = true, columns: []const Column = &.{}, panes: []const Pane = &.{}, diff --git a/src/locations.zig b/src/locations.zig index b34f7a6f..6a604907 100644 --- a/src/locations.zig +++ b/src/locations.zig @@ -4,7 +4,6 @@ const look = @import("look.zig"); const syntax = @import("syntax.zig"); const filesystem = @import("fs.zig"); -pub const Config = @import("locations_config.zig").Config; const neighbor_context: u8 = 1; const declaration_context: u8 = 2; const declaration_start: u8 = 4; @@ -206,7 +205,7 @@ pub fn format(p: *pardes.Pardes, dir: []const u8, input: []const u8, anchor: ?us // Muted declaration headers do not need a source-color pass. // Exact bytes include unsaved edits and virtual/remote sources. var cached = p.locations_cache.get(p.tree_sitter_gpa, text.path, source, p.locations_config.tscontext, p.locations_config.context > 0) catch - @import("locations_cache.zig").Result{ .analysis = syntax.analyzeSource(arena, text.path, source, p.locations_config.tscontext, p.locations_config.context > 0) catch .{} }; + Cache.Hit{ .analysis = syntax.analyzeSource(arena, text.path, source, p.locations_config.tscontext, p.locations_config.context > 0) catch .{} }; defer cached.deinit(p.tree_sitter_gpa); const analysis = cached.analysis; const source_colors = analysis.colors; @@ -393,3 +392,292 @@ test "locations native results retain the first source tab" { try std.testing.expectEqualStrings("some directory/a.zig", row.path); try std.testing.expectEqualStrings("\t value", text[row.code_start..]); } + +// ---- the Locations setting ---- + +pub const Config = struct { + pub const Layout = enum { @"inline", stacked }; + context: u16 = 0, + tscontext: bool = false, + tslocations: bool = true, + layout: Layout = .stacked, + + /// Apply only supplied fields. A malformed token rejects the whole update. + /// Repeating a field uses its last supplied value. + pub fn parse(current: Config, text: []const u8) !Config { + var result = current; + var tokens = std.mem.tokenizeAny(u8, text, " \t\r\n"); + while (tokens.next()) |token| { + const colon = std.mem.indexOfScalar(u8, token, ':') orelse return error.ExpectedKeyValue; + const key = token[0..colon]; + const value = token[colon + 1 ..]; + var found = false; + inline for (@typeInfo(Config).@"struct".fields) |field| { + if (std.mem.eql(u8, key, field.name)) { + @field(result, field.name) = switch (@typeInfo(field.type)) { + .bool => if (std.mem.eql(u8, value, "on")) true else if (std.mem.eql(u8, value, "off")) false else return error.ExpectedOnOrOff, + .int => std.fmt.parseInt(field.type, value, 10) catch return error.InvalidContext, + .@"enum" => std.meta.stringToEnum(field.type, value) orelse return error.InvalidLayout, + else => @compileError("unsupported location setting type"), + }; + found = true; + } + } + if (!found) return error.UnknownLocationSetting; + } + return result; + } + + pub fn write(config: Config, writer: *std.Io.Writer) !void { + inline for (@typeInfo(Config).@"struct".fields, 0..) |field, index| { + if (index > 0) try writer.writeByte(' '); + try writer.writeAll(field.name ++ ":"); + const value = @field(config, field.name); + switch (@typeInfo(field.type)) { + .bool => try writer.writeAll(if (value) "on" else "off"), + .int => try writer.print("{d}", .{value}), + .@"enum" => try writer.writeAll(@tagName(value)), + else => @compileError("unsupported location setting type"), + } + } + } +}; + +test "LocationsConfig reflection round trip and partial updates" { + var config: Config = .{}; + config = try config.parse("context:5 tscontext:on"); + config = try config.parse("tslocations:off"); + try std.testing.expectEqual(Config{ .context = 5, .tscontext = true, .tslocations = false }, config); + var out: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer out.deinit(); + try config.write(&out.writer); + try std.testing.expectEqualStrings("context:5 tscontext:on tslocations:off layout:stacked", out.written()); + try std.testing.expectEqual(config, try (Config{}).parse(out.written())); + try std.testing.expectEqual(config, try config.parse(" \n\t")); + try std.testing.expectEqual(@as(u16, 2), (try config.parse("context:1 context:2")).context); + try std.testing.expectEqual(Config.Layout.@"inline", (try config.parse("layout:inline")).layout); +} + +test "LocationsConfig rejects invalid updates atomically" { + const config: Config = .{ .context = 3, .tscontext = true }; + try std.testing.expectError(error.UnknownLocationSetting, config.parse("context:5 unknown:on")); + try std.testing.expectError(error.ExpectedOnOrOff, config.parse("context:5 tscontext:true")); + try std.testing.expectError(error.ExpectedKeyValue, config.parse("context")); + try std.testing.expectError(error.InvalidLayout, config.parse("context:5 layout:sideways")); + for ([_][]const u8{ "context:", "context:-1", "context:65536", "context:five" }) |input| + try std.testing.expectError(error.InvalidContext, config.parse(input)); + try std.testing.expectEqual(@as(u16, 3), config.context); +} + +// ---- the analysis cache ---- + +/// Owned source snapshots keep analysis valid across filesystem changes and +/// borrowed editor buffers. Entries are ordered from least to most recent. +pub const Cache = struct { + /// A cache hit borrows its analysis; an entry too large to retain transfers + /// ownership instead. Call deinit after consuming either result. + pub const Hit = struct { + analysis: syntax.SourceAnalysis, + owned: bool = false, + + pub fn deinit(result: *Hit, gpa: std.mem.Allocator) void { + if (result.owned) result.analysis.deinit(gpa); + result.* = undefined; + } + }; + + pub const max_bytes = 64 * 1024 * 1024; + pub const max_entries = 64; + + const Entry = struct { + path: []u8, + source: []u8, + analysis: syntax.SourceAnalysis, + declarations_ready: bool, + colors_ready: bool, + + fn size(entry: Entry) usize { + return entry.path.len + entry.source.len + entry.analysis.colors.len + + entry.analysis.declarations.len * @sizeOf(syntax.ContextDeclaration); + } + + fn deinit(entry: *Entry, gpa: std.mem.Allocator) void { + gpa.free(entry.path); + gpa.free(entry.source); + entry.analysis.deinit(gpa); + } + }; + + entries: [max_entries]Entry = undefined, + len: usize = 0, + bytes: usize = 0, + analyses: usize = 0, + hits: usize = 0, + + pub fn deinit(cache: *Cache, gpa: std.mem.Allocator) void { + for (cache.entries[0..cache.len]) |*entry| entry.deinit(gpa); + cache.* = .{}; + } + + /// Borrowed slices remain valid until the next get or cache deinit; an + /// owned result remains valid until Hit.deinit. All calls for this + /// cache must use the same allocator. Unsupported languages + /// are cached too, including their empty analysis arrays. + pub fn get(cache: *Cache, gpa: std.mem.Allocator, path: []const u8, source: []const u8, want_declarations: bool, want_colors: bool) !Hit { + return cache.getBounded(gpa, path, source, want_declarations, want_colors, max_bytes); + } + + fn getBounded(cache: *Cache, gpa: std.mem.Allocator, path: []const u8, source: []const u8, want_declarations: bool, want_colors: bool, budget: usize) !Hit { + if (source.len > budget or path.len > budget - source.len) return error.SourceTooLarge; + var previous: ?usize = null; + var declarations = want_declarations; + var colors = want_colors; + for (cache.entries[0..cache.len], 0..) |entry, index| { + if (!std.mem.eql(u8, entry.path, path)) continue; + previous = index; + if (!std.mem.eql(u8, entry.source, source)) break; + if ((!want_declarations or entry.declarations_ready) and (!want_colors or entry.colors_ready)) { + // Moving the small ownership record keeps the underlying + // allocations intact and avoids timestamp/overflow state. + std.mem.copyForwards(Entry, cache.entries[index .. cache.len - 1], cache.entries[index + 1 .. cache.len]); + cache.entries[cache.len - 1] = entry; + cache.hits +|= 1; + return .{ .analysis = entry.analysis }; + } + declarations = declarations or entry.declarations_ready; + colors = colors or entry.colors_ready; + break; + } + // A supported full-source color map needs one byte per source byte. + // Reject that known minimum before parsing, so the caller's uncached + // path does not repeat an expensive analysis for oversized inputs. + const input_bytes = path.len + source.len; + if (colors and syntax.supportsPath(path) and source.len > budget - input_bytes) return error.SourceTooLarge; + var analysis = try syntax.analyzeSource(gpa, path, source, declarations, colors); + errdefer analysis.deinit(gpa); + cache.analyses +|= 1; + const analysis_bytes = analysis.colors.len + analysis.declarations.len * @sizeOf(syntax.ContextDeclaration); + if (analysis_bytes > budget - input_bytes) return .{ .analysis = analysis, .owned = true }; + const owned_path = try gpa.dupe(u8, path); + errdefer gpa.free(owned_path); + const owned_source = try gpa.dupe(u8, source); + errdefer gpa.free(owned_source); + const next: Entry = .{ + .path = owned_path, + .source = owned_source, + .analysis = analysis, + .declarations_ready = declarations, + .colors_ready = colors, + }; + // Commit only after every allocation succeeds. A failed refresh leaves + // the previous cached snapshot usable by the next request. + if (previous) |index| cache.remove(gpa, index); + while (cache.len == max_entries or cache.bytes > budget - next.size()) cache.remove(gpa, 0); + cache.entries[cache.len] = next; + cache.len += 1; + cache.bytes += next.size(); + return .{ .analysis = next.analysis }; + } + + fn remove(cache: *Cache, gpa: std.mem.Allocator, index: usize) void { + cache.bytes -= cache.entries[index].size(); + cache.entries[index].deinit(gpa); + std.mem.copyForwards(Entry, cache.entries[index .. cache.len - 1], cache.entries[index + 1 .. cache.len]); + cache.len -= 1; + } +}; + +test "locations cache validates exact source and path and upgrades analysis" { + const gpa = std.testing.allocator; + syntax.start(gpa); + defer syntax.stop(); + var cache: Cache = .{}; + defer cache.deinit(gpa); + const source = "const Thing = struct {\n value: u32,\n};\n"; + _ = try cache.get(gpa, "a.zig", source, true, false); + try std.testing.expectEqual(@as(usize, 1), cache.analyses); + _ = try cache.get(gpa, "a.zig", source, true, false); + try std.testing.expectEqual(@as(usize, 1), cache.analyses); + try std.testing.expectEqual(@as(usize, 1), cache.hits); + _ = try cache.get(gpa, "a.zig", source, false, true); + try std.testing.expectEqual(@as(usize, 2), cache.analyses); + try std.testing.expect(cache.entries[0].declarations_ready and cache.entries[0].colors_ready); + _ = try cache.get(gpa, "a.zig", source, true, true); + try std.testing.expectEqual(@as(usize, 2), cache.analyses); + _ = try cache.get(gpa, "a.zig", "const Thing = struct {\n other: u32,\n};\n", true, true); + try std.testing.expectEqual(@as(usize, 3), cache.analyses); + try std.testing.expectEqual(@as(usize, 1), cache.len); + _ = try cache.get(gpa, "a.txt", source, true, true); + try std.testing.expectEqual(@as(usize, 4), cache.analyses); + try std.testing.expectEqual(@as(usize, 2), cache.len); +} + +test "locations cache bounds memory and entries and evicts least recently used" { + const gpa = std.testing.allocator; + var cache: Cache = .{}; + defer cache.deinit(gpa); + for (0..Cache.max_entries) |index| { + var path: [32]u8 = undefined; + _ = try cache.get(gpa, try std.fmt.bufPrint(&path, "{d}.unknown", .{index}), "source", false, false); + } + _ = try cache.get(gpa, "0.unknown", "source", false, false); + _ = try cache.get(gpa, "new.unknown", "source", false, false); + try std.testing.expectEqual(Cache.max_entries, cache.len); + try std.testing.expectEqualStrings("2.unknown", cache.entries[0].path); + const count = cache.analyses; + _ = try cache.get(gpa, "0.unknown", "source", false, false); + try std.testing.expectEqual(count, cache.analyses); + cache.deinit(gpa); + _ = try cache.getBounded(gpa, "a.unknown", "source", false, false, 40); + _ = try cache.getBounded(gpa, "b.unknown", "source", false, false, 40); + _ = try cache.getBounded(gpa, "c.unknown", "source", false, false, 40); + try std.testing.expectEqual(@as(usize, 2), cache.len); + try std.testing.expect(cache.bytes <= 40); + try std.testing.expectEqualStrings("b.unknown", cache.entries[0].path); + try std.testing.expectError(error.SourceTooLarge, cache.getBounded(gpa, "large.unknown", "x" ** 41, false, false, 40)); + try std.testing.expectEqual(@as(usize, 2), cache.len); + if (syntax.supportsPath("large.zig")) { + const before = cache.analyses; + try std.testing.expectError(error.SourceTooLarge, cache.getBounded(gpa, "large.zig", "x" ** 25, true, true, 40)); + try std.testing.expectEqual(before, cache.analyses); + } + cache.deinit(gpa); + try std.testing.expectEqual(@as(usize, 0), cache.bytes); + try std.testing.expectEqual(@as(usize, 0), cache.len); +} + +test "locations cache keeps its previous snapshot after allocation failure" { + var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{}); + const gpa = failing.allocator(); + var cache: Cache = .{}; + defer cache.deinit(gpa); + _ = try cache.get(gpa, "a.unknown", "old bytes", false, false); + const retained = cache.bytes; + failing.fail_index = failing.alloc_index; + try std.testing.expectError(error.OutOfMemory, cache.get(gpa, "a.unknown", "new bytes", false, false)); + try std.testing.expectEqual(@as(usize, 1), cache.len); + try std.testing.expectEqual(retained, cache.bytes); + try std.testing.expectEqualStrings("old bytes", cache.entries[0].source); + // This succeeds with allocations still disabled because the old entry + // remains valid and the hit path moves ownership records only. + _ = try cache.get(gpa, "a.unknown", "old bytes", false, false); + try std.testing.expectEqual(@as(usize, 1), cache.hits); +} + +test "locations cache transfers oversized completed analysis without reparsing" { + if (!syntax.supportsPath("large.zig")) return error.SkipZigTest; + const gpa = std.testing.allocator; + syntax.start(gpa); + defer syntax.stop(); + var cache: Cache = .{}; + defer cache.deinit(gpa); + const source = "const Thing = struct {\n value: u32,\n};\n"; + const budget = "large.zig".len + source.len; + var result = try cache.getBounded(gpa, "large.zig", source, true, false, budget); + defer result.deinit(gpa); + try std.testing.expect(result.owned); + try std.testing.expect(result.analysis.declarations.len > 0); + try std.testing.expectEqual(@as(usize, 1), cache.analyses); + try std.testing.expectEqual(@as(usize, 0), cache.len); + try std.testing.expectEqual(@as(usize, 0), cache.bytes); +} diff --git a/src/locations_cache.zig b/src/locations_cache.zig deleted file mode 100644 index a079226b..00000000 --- a/src/locations_cache.zig +++ /dev/null @@ -1,213 +0,0 @@ -const std = @import("std"); -const syntax = @import("syntax.zig"); - -/// A cache hit borrows its analysis; an entry too large to retain transfers -/// ownership instead. Call deinit after consuming either result. -pub const Result = struct { - analysis: syntax.SourceAnalysis, - owned: bool = false, - - pub fn deinit(result: *Result, gpa: std.mem.Allocator) void { - if (result.owned) result.analysis.deinit(gpa); - result.* = undefined; - } -}; - -/// Owned source snapshots keep analysis valid across filesystem changes and -/// borrowed editor buffers. Entries are ordered from least to most recent. -pub const Cache = struct { - pub const max_bytes = 64 * 1024 * 1024; - pub const max_entries = 64; - - const Entry = struct { - path: []u8, - source: []u8, - analysis: syntax.SourceAnalysis, - declarations_ready: bool, - colors_ready: bool, - - fn size(entry: Entry) usize { - return entry.path.len + entry.source.len + entry.analysis.colors.len + - entry.analysis.declarations.len * @sizeOf(syntax.ContextDeclaration); - } - - fn deinit(entry: *Entry, gpa: std.mem.Allocator) void { - gpa.free(entry.path); - gpa.free(entry.source); - entry.analysis.deinit(gpa); - } - }; - - entries: [max_entries]Entry = undefined, - len: usize = 0, - bytes: usize = 0, - analyses: usize = 0, - hits: usize = 0, - - pub fn deinit(cache: *Cache, gpa: std.mem.Allocator) void { - for (cache.entries[0..cache.len]) |*entry| entry.deinit(gpa); - cache.* = .{}; - } - - /// Borrowed slices remain valid until the next get or cache deinit; an - /// owned result remains valid until Result.deinit. All calls for this - /// cache must use the same allocator. Unsupported languages - /// are cached too, including their empty analysis arrays. - pub fn get(cache: *Cache, gpa: std.mem.Allocator, path: []const u8, source: []const u8, want_declarations: bool, want_colors: bool) !Result { - return cache.getBounded(gpa, path, source, want_declarations, want_colors, max_bytes); - } - - fn getBounded(cache: *Cache, gpa: std.mem.Allocator, path: []const u8, source: []const u8, want_declarations: bool, want_colors: bool, budget: usize) !Result { - if (source.len > budget or path.len > budget - source.len) return error.SourceTooLarge; - var previous: ?usize = null; - var declarations = want_declarations; - var colors = want_colors; - for (cache.entries[0..cache.len], 0..) |entry, index| { - if (!std.mem.eql(u8, entry.path, path)) continue; - previous = index; - if (!std.mem.eql(u8, entry.source, source)) break; - if ((!want_declarations or entry.declarations_ready) and (!want_colors or entry.colors_ready)) { - // Moving the small ownership record keeps the underlying - // allocations intact and avoids timestamp/overflow state. - std.mem.copyForwards(Entry, cache.entries[index .. cache.len - 1], cache.entries[index + 1 .. cache.len]); - cache.entries[cache.len - 1] = entry; - cache.hits +|= 1; - return .{ .analysis = entry.analysis }; - } - declarations = declarations or entry.declarations_ready; - colors = colors or entry.colors_ready; - break; - } - // A supported full-source color map needs one byte per source byte. - // Reject that known minimum before parsing, so the caller's uncached - // path does not repeat an expensive analysis for oversized inputs. - const input_bytes = path.len + source.len; - if (colors and syntax.supportsPath(path) and source.len > budget - input_bytes) return error.SourceTooLarge; - var analysis = try syntax.analyzeSource(gpa, path, source, declarations, colors); - errdefer analysis.deinit(gpa); - cache.analyses +|= 1; - const analysis_bytes = analysis.colors.len + analysis.declarations.len * @sizeOf(syntax.ContextDeclaration); - if (analysis_bytes > budget - input_bytes) return .{ .analysis = analysis, .owned = true }; - const owned_path = try gpa.dupe(u8, path); - errdefer gpa.free(owned_path); - const owned_source = try gpa.dupe(u8, source); - errdefer gpa.free(owned_source); - const next: Entry = .{ - .path = owned_path, - .source = owned_source, - .analysis = analysis, - .declarations_ready = declarations, - .colors_ready = colors, - }; - // Commit only after every allocation succeeds. A failed refresh leaves - // the previous cached snapshot usable by the next request. - if (previous) |index| cache.remove(gpa, index); - while (cache.len == max_entries or cache.bytes > budget - next.size()) cache.remove(gpa, 0); - cache.entries[cache.len] = next; - cache.len += 1; - cache.bytes += next.size(); - return .{ .analysis = next.analysis }; - } - - fn remove(cache: *Cache, gpa: std.mem.Allocator, index: usize) void { - cache.bytes -= cache.entries[index].size(); - cache.entries[index].deinit(gpa); - std.mem.copyForwards(Entry, cache.entries[index .. cache.len - 1], cache.entries[index + 1 .. cache.len]); - cache.len -= 1; - } -}; - -test "locations cache validates exact source and path and upgrades analysis" { - const gpa = std.testing.allocator; - syntax.start(gpa); - defer syntax.stop(); - var cache: Cache = .{}; - defer cache.deinit(gpa); - const source = "const Thing = struct {\n value: u32,\n};\n"; - _ = try cache.get(gpa, "a.zig", source, true, false); - try std.testing.expectEqual(@as(usize, 1), cache.analyses); - _ = try cache.get(gpa, "a.zig", source, true, false); - try std.testing.expectEqual(@as(usize, 1), cache.analyses); - try std.testing.expectEqual(@as(usize, 1), cache.hits); - _ = try cache.get(gpa, "a.zig", source, false, true); - try std.testing.expectEqual(@as(usize, 2), cache.analyses); - try std.testing.expect(cache.entries[0].declarations_ready and cache.entries[0].colors_ready); - _ = try cache.get(gpa, "a.zig", source, true, true); - try std.testing.expectEqual(@as(usize, 2), cache.analyses); - _ = try cache.get(gpa, "a.zig", "const Thing = struct {\n other: u32,\n};\n", true, true); - try std.testing.expectEqual(@as(usize, 3), cache.analyses); - try std.testing.expectEqual(@as(usize, 1), cache.len); - _ = try cache.get(gpa, "a.txt", source, true, true); - try std.testing.expectEqual(@as(usize, 4), cache.analyses); - try std.testing.expectEqual(@as(usize, 2), cache.len); -} - -test "locations cache bounds memory and entries and evicts least recently used" { - const gpa = std.testing.allocator; - var cache: Cache = .{}; - defer cache.deinit(gpa); - for (0..Cache.max_entries) |index| { - var path: [32]u8 = undefined; - _ = try cache.get(gpa, try std.fmt.bufPrint(&path, "{d}.unknown", .{index}), "source", false, false); - } - _ = try cache.get(gpa, "0.unknown", "source", false, false); - _ = try cache.get(gpa, "new.unknown", "source", false, false); - try std.testing.expectEqual(Cache.max_entries, cache.len); - try std.testing.expectEqualStrings("2.unknown", cache.entries[0].path); - const count = cache.analyses; - _ = try cache.get(gpa, "0.unknown", "source", false, false); - try std.testing.expectEqual(count, cache.analyses); - cache.deinit(gpa); - _ = try cache.getBounded(gpa, "a.unknown", "source", false, false, 40); - _ = try cache.getBounded(gpa, "b.unknown", "source", false, false, 40); - _ = try cache.getBounded(gpa, "c.unknown", "source", false, false, 40); - try std.testing.expectEqual(@as(usize, 2), cache.len); - try std.testing.expect(cache.bytes <= 40); - try std.testing.expectEqualStrings("b.unknown", cache.entries[0].path); - try std.testing.expectError(error.SourceTooLarge, cache.getBounded(gpa, "large.unknown", "x" ** 41, false, false, 40)); - try std.testing.expectEqual(@as(usize, 2), cache.len); - if (syntax.supportsPath("large.zig")) { - const before = cache.analyses; - try std.testing.expectError(error.SourceTooLarge, cache.getBounded(gpa, "large.zig", "x" ** 25, true, true, 40)); - try std.testing.expectEqual(before, cache.analyses); - } - cache.deinit(gpa); - try std.testing.expectEqual(@as(usize, 0), cache.bytes); - try std.testing.expectEqual(@as(usize, 0), cache.len); -} - -test "locations cache keeps its previous snapshot after allocation failure" { - var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{}); - const gpa = failing.allocator(); - var cache: Cache = .{}; - defer cache.deinit(gpa); - _ = try cache.get(gpa, "a.unknown", "old bytes", false, false); - const retained = cache.bytes; - failing.fail_index = failing.alloc_index; - try std.testing.expectError(error.OutOfMemory, cache.get(gpa, "a.unknown", "new bytes", false, false)); - try std.testing.expectEqual(@as(usize, 1), cache.len); - try std.testing.expectEqual(retained, cache.bytes); - try std.testing.expectEqualStrings("old bytes", cache.entries[0].source); - // This succeeds with allocations still disabled because the old entry - // remains valid and the hit path moves ownership records only. - _ = try cache.get(gpa, "a.unknown", "old bytes", false, false); - try std.testing.expectEqual(@as(usize, 1), cache.hits); -} - -test "locations cache transfers oversized completed analysis without reparsing" { - if (!syntax.supportsPath("large.zig")) return error.SkipZigTest; - const gpa = std.testing.allocator; - syntax.start(gpa); - defer syntax.stop(); - var cache: Cache = .{}; - defer cache.deinit(gpa); - const source = "const Thing = struct {\n value: u32,\n};\n"; - const budget = "large.zig".len + source.len; - var result = try cache.getBounded(gpa, "large.zig", source, true, false, budget); - defer result.deinit(gpa); - try std.testing.expect(result.owned); - try std.testing.expect(result.analysis.declarations.len > 0); - try std.testing.expectEqual(@as(usize, 1), cache.analyses); - try std.testing.expectEqual(@as(usize, 0), cache.len); - try std.testing.expectEqual(@as(usize, 0), cache.bytes); -} diff --git a/src/locations_config.zig b/src/locations_config.zig deleted file mode 100644 index 6b23575a..00000000 --- a/src/locations_config.zig +++ /dev/null @@ -1,75 +0,0 @@ -const std = @import("std"); - -pub const Config = struct { - pub const Layout = enum { @"inline", stacked }; - context: u16 = 0, - tscontext: bool = false, - tslocations: bool = true, - layout: Layout = .stacked, - - /// Apply only supplied fields. A malformed token rejects the whole update. - /// Repeating a field uses its last supplied value. - pub fn parse(current: Config, text: []const u8) !Config { - var result = current; - var tokens = std.mem.tokenizeAny(u8, text, " \t\r\n"); - while (tokens.next()) |token| { - const colon = std.mem.indexOfScalar(u8, token, ':') orelse return error.ExpectedKeyValue; - const key = token[0..colon]; - const value = token[colon + 1 ..]; - var found = false; - inline for (@typeInfo(Config).@"struct".fields) |field| { - if (std.mem.eql(u8, key, field.name)) { - @field(result, field.name) = switch (@typeInfo(field.type)) { - .bool => if (std.mem.eql(u8, value, "on")) true else if (std.mem.eql(u8, value, "off")) false else return error.ExpectedOnOrOff, - .int => std.fmt.parseInt(field.type, value, 10) catch return error.InvalidContext, - .@"enum" => std.meta.stringToEnum(field.type, value) orelse return error.InvalidLayout, - else => @compileError("unsupported location setting type"), - }; - found = true; - } - } - if (!found) return error.UnknownLocationSetting; - } - return result; - } - - pub fn write(config: Config, writer: *std.Io.Writer) !void { - inline for (@typeInfo(Config).@"struct".fields, 0..) |field, index| { - if (index > 0) try writer.writeByte(' '); - try writer.writeAll(field.name ++ ":"); - const value = @field(config, field.name); - switch (@typeInfo(field.type)) { - .bool => try writer.writeAll(if (value) "on" else "off"), - .int => try writer.print("{d}", .{value}), - .@"enum" => try writer.writeAll(@tagName(value)), - else => @compileError("unsupported location setting type"), - } - } - } -}; - -test "LocationsConfig reflection round trip and partial updates" { - var config: Config = .{}; - config = try config.parse("context:5 tscontext:on"); - config = try config.parse("tslocations:off"); - try std.testing.expectEqual(Config{ .context = 5, .tscontext = true, .tslocations = false }, config); - var out: std.Io.Writer.Allocating = .init(std.testing.allocator); - defer out.deinit(); - try config.write(&out.writer); - try std.testing.expectEqualStrings("context:5 tscontext:on tslocations:off layout:stacked", out.written()); - try std.testing.expectEqual(config, try (Config{}).parse(out.written())); - try std.testing.expectEqual(config, try config.parse(" \n\t")); - try std.testing.expectEqual(@as(u16, 2), (try config.parse("context:1 context:2")).context); - try std.testing.expectEqual(Config.Layout.@"inline", (try config.parse("layout:inline")).layout); -} - -test "LocationsConfig rejects invalid updates atomically" { - const config: Config = .{ .context = 3, .tscontext = true }; - try std.testing.expectError(error.UnknownLocationSetting, config.parse("context:5 unknown:on")); - try std.testing.expectError(error.ExpectedOnOrOff, config.parse("context:5 tscontext:true")); - try std.testing.expectError(error.ExpectedKeyValue, config.parse("context")); - try std.testing.expectError(error.InvalidLayout, config.parse("context:5 layout:sideways")); - for ([_][]const u8{ "context:", "context:-1", "context:65536", "context:five" }) |input| - try std.testing.expectError(error.InvalidContext, config.parse(input)); - try std.testing.expectEqual(@as(u16, 3), config.context); -} diff --git a/src/main.zig b/src/main.zig index efc24a11..6ea05479 100644 --- a/src/main.zig +++ b/src/main.zig @@ -521,7 +521,7 @@ test { _ = pardes.config.User; _ = @import("crash.zig"); _ = @import("memory.zig"); - _ = @import("locations_cache.zig"); + _ = @import("locations.zig"); _ = @import("fs.zig"); _ = @import("detached/wire.zig"); _ = @import("ShaderBuild.zig"); diff --git a/src/pardes.zig b/src/pardes.zig index 90095556..141b214e 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -9,9 +9,9 @@ pub const look = @import("look.zig"); pub const filesystem = @import("fs.zig"); pub const ctlfs = @import("ninep/tree.zig"); pub const syntax = @import("syntax.zig"); -pub const locations_config = @import("locations_config.zig"); const tracy = @import("tracy.zig"); pub const panes = @import("panes.zig"); +const locations = @import("locations.zig"); pub const tagline = @import("tagline.zig"); pub const exec = @import("exec.zig"); const recent_files = @import("recent.zig"); @@ -766,7 +766,7 @@ test "LocationsConfig command reports partial updates and survives restore" { _ = try p.setTestFile("source\n"); try std.testing.expect(p.executeBuiltinLine(0, "LocationsConfig context:5 tscontext:on")); try std.testing.expect(p.executeBuiltinLine(0, "LocationsConfig tslocations:off")); - const expected: locations_config.Config = .{ .context = 5, .tscontext = true, .tslocations = false }; + const expected: locations.Config = .{ .context = 5, .tscontext = true, .tslocations = false }; try std.testing.expectEqual(expected, p.locations_config); try std.testing.expect(p.executeBuiltinLine(0, "LocationsConfig context:2 unknown:on")); try std.testing.expectEqual(expected, p.locations_config); @@ -4392,8 +4392,8 @@ pub const Pardes = struct { /// (exec.placeNew). A Look click moves the keyboard but not this. active_column: u32 = 0, settings: config.Runtime = .{ .font = .{ .tagline_percent = config.gui_tagline_font_percent } }, - locations_config: locations_config.Config = .{}, - locations_cache: @import("locations_cache.zig").Cache = .{}, + locations_config: locations.Config = .{}, + locations_cache: locations.Cache = .{}, tty_filter_palette: panes.Terminal.FilterPalette = .{}, font_request_taken: bool = false, custom_theme: ?Theme = null, |
