const std = @import("std"); const pardes = @import("pardes"); const panes = pardes.panes; const config = pardes.config; const builtins = pardes.builtins; const modal = pardes.modal; const Pardes = pardes.Pardes; const Key = pardes.Key; const platform = pardes.platform; const font_picker = pardes.font_picker; const fonts = if (font_picker) pardes.fonts else struct {}; // Sorting tests compare original locations and source bytes independently of // alignment. Dedicated Locations tests verify the rendered column widths. fn expectResultRows(expected: []const u8, file: *const panes.File.State) !void { if (file.location_rows.len == 0) return std.testing.expectEqualStrings(expected, file.content); try std.testing.expectEqual(std.mem.endsWith(u8, expected, "\n"), std.mem.endsWith(u8, file.content, "\n")); var source = std.mem.splitScalar(u8, expected, '\n'); var shown = std.mem.splitScalar(u8, file.content, '\n'); for (file.location_rows) |row| { const want = source.next() orelse return error.MissingExpectedResult; const actual = shown.next() orelse return error.MissingRenderedResult; const target = panes.Output.location(want); try std.testing.expectEqualStrings(want[0..target.end], actual[0..row.location_end]); try std.testing.expectEqualStrings(want[@min(want.len, target.end + 1)..], actual[row.code_start..]); } try std.testing.expectEqualStrings("", source.next() orelse ""); try std.testing.expect(source.next() == null); } test "search tint survives wrapping and theme changes while selections remain authoritative" { const text = "nested/path/a.txt:1:5-7 let abc = 1;\n"; for ([_]u16{ 30, 80 }) |width| { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = width, .rows = 12 }); defer p.deinit(); const pane = try p.setTestFile(text); pane.file.?.output = .{ .from = .search }; var frame = std.heap.ArenaAllocator.init(std.testing.allocator); defer frame.deinit(); for ([_]bool{ false, true }) |bottom| { p.settings.tag_bottom = bottom; for ([_]bool{ true, false, true }) |colors| { p.settings.colors = colors; const surface = try p.render(frame.allocator()); var marked: usize = 0; for (surface.cells) |cell| { if (std.meta.eql(cell.style.bg, pardes.Color{ .rgb = p.theme().search_bg.? })) marked += 1; } try std.testing.expectEqual(@as(usize, if (colors) 3 else 0), marked); _ = frame.reset(.retain_capacity); } } // The body changes immediately even when dark chrome is still fading. try std.testing.expect(p.executeBuiltinLine(0, "Theme daybreak")); const light = try p.render(frame.allocator()); var light_matches: usize = 0; for (light.cells) |cell| { if (!std.meta.eql(cell.style.bg, pardes.Color{ .rgb = p.theme().search_bg.? })) continue; try std.testing.expectEqualDeep(pardes.Color{ .rgb = p.theme().search_fg.? }, cell.style.fg); light_matches += 1; } try std.testing.expectEqual(@as(usize, 3), light_matches); pane.body.mode = .normal; pane.body.cur_row = 0; pane.body.cur_col = @intCast(std.mem.indexOf(u8, text, "abc").? + 2); pane.body.vsel = .{ .active = true, .row = 0, .col = pane.body.cur_col - 2, .explicit = true }; const selected = try p.render(frame.allocator()); for (selected.cells) |cell| try std.testing.expect(!std.meta.eql(cell.style.bg, pardes.Color{ .rgb = p.theme().search_bg.? })); try std.testing.expectEqualStrings(text, pane.file.?.content); } } fn outputCell(p: *Pardes, surface: *pardes.Surface, id: usize, byte: usize) *pardes.Cell { const pane = p.panes[id].?; const line = modal.lineSlice(pane.file.?.content, 0); const rect = p.rects[id]; const x = rect.x + config.GUTTER + panes.File.gutterWidth(pane) + @as(u16, @intCast(panes.File.displayWidth(line[0..byte]))); const y = p.bodyTop(pane, rect); return surface.at(x, y); } fn resultId(p: *Pardes) !usize { for (p.panes, 0..) |slot, id| if (slot) |pane| { if (pane.file) |file| if (file.output != null) return id; }; return error.MissingOutput; } test "output callers opt into location highlighting instead of matching prose colons" { for ([_]panes.Output.Origin{ .{ .cmd = .LocationsConfig }, .{ .cmd = .Config }, .{ .query = .hover } }) |origin| { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 140, .rows = 30 }); defer p.deinit(); p.locations_config.layout = .@"inline"; _ = try p.setTestFile("ordinary\n"); p.presentation.enabled = false; if (origin == .cmd and origin.cmd == .LocationsConfig) { p.locations_config.context = 5; try std.testing.expect(p.executeBuiltinLine(0, "LocationsConfig")); } else { try panes.Output.fillResults(p, 0, "/", origin, "plain report", try p.gpa.dupe(u8, "source.zig:3:5-7 warning: context:5\n"), null); } const id = try resultId(p); const file = &p.panes[id].?.file.?; try std.testing.expectEqual(@as(usize, 0), file.location_rows.len); pardes.layout.compute(p); var frame = std.heap.ArenaAllocator.init(p.gpa); defer frame.deinit(); const surface = try p.render(frame.allocator()); const line = modal.lineSlice(file.content, 0); const before_colon = std.mem.indexOfScalar(u8, line, ':').?; const normal: pardes.Color = if (p.theme().fg) |fg| .{ .rgb = fg } else .default; for (0..before_colon + 2) |byte| { const cell = outputCell(p, surface, id, byte); try std.testing.expectEqual(normal, cell.style.fg); try std.testing.expect(!cell.style.bold); } } } test "location producers retain rendered location match and diagnostic styles" { for ([_]panes.Output.Origin{ .search, .{ .cmd = .Grep }, .{ .query = .references }, .{ .query = .diagnostics } }) |origin| { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 140, .rows = 30 }); defer p.deinit(); p.locations_config.layout = .@"inline"; _ = try p.setTestFile("ordinary\n"); p.presentation.enabled = false; const diagnostic = origin == .query and origin.query == .diagnostics; const code = if (diagnostic) "warning: unused value" else "const value = 42;"; try panes.Output.fillResults(p, 0, "/", origin, "styles", try std.fmt.allocPrint(p.gpa, "source.zig:3:1-3 {s}\n", .{code}), null); const id = try resultId(p); const file = &p.panes[id].?.file.?; try std.testing.expectEqual(@as(usize, 1), file.location_rows.len); pardes.layout.compute(p); var frame = std.heap.ArenaAllocator.init(p.gpa); defer frame.deinit(); const surface = try p.render(frame.allocator()); const chrome = pardes.ChromeTheme.fromTheme(p.theme()); try std.testing.expectEqual(pardes.Color{ .rgb = chrome.lineno }, outputCell(p, surface, id, 0).style.fg); const start = file.location_rows[0].code_start; const first = outputCell(p, surface, id, start); if (diagnostic) { try std.testing.expectEqual(pardes.Color{ .rgb = chrome.diagnostic_warning }, first.style.fg); try std.testing.expect(first.style.bold); } else if (origin == .search or origin == .cmd) { try std.testing.expectEqual(pardes.Color{ .rgb = chrome.search_bg }, first.style.bg); } if (!diagnostic and pardes.syntax.enabled) { const number = start + std.mem.indexOf(u8, code, "42").?; try std.testing.expectEqual(pardes.Color{ .rgb = p.theme().num }, outputCell(p, surface, id, number).style.fg); } } } test "location results sort filenames and numeric positions while remapping their anchor" { const before = "z.zig:1:1 z\nb.zig:10:2-3 ten\nb.zig:2:10-12 two-b\nb.zig:2:2-3 two-a\na.zig:99:1 a\nb.zig:2:2-3 duplicate\n"; const after = "a.zig:99:1 a\nb.zig:2:2-3 two-a\nb.zig:2:2-3 duplicate\nb.zig:2:10-12 two-b\nb.zig:10:2-3 ten\nz.zig:1:1 z\n"; for ([_]panes.Output.Origin{ .search, .{ .cmd = .Grep }, .{ .query = .references }, .{ .query = .diagnostics } }) |origin| { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); defer p.deinit(); p.locations_config.layout = .@"inline"; const source = try p.setTestFile("notes\n"); try panes.Output.fillResults(p, 0, "/tmp", origin, "", try p.gpa.dupe(u8, before), 2); const results = p.panes[source.search_pane.?].?; try expectResultRows(after, &results.file.?); try std.testing.expectEqual(@as(usize, 3), source.search_row.?); } } test "ranked choices and prose retain their supplied order" { const text = "z.zig:10:1 first\na.zig:2:1 second\n"; for ([_]pardes.lsp.Kind{ .completion, .hover, .code_action }) |kind| { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); defer p.deinit(); p.locations_config.layout = .@"inline"; _ = try p.setTestFile("notes\n"); try panes.Output.fillResults(p, 0, "/tmp", .{ .query = kind }, "", try p.gpa.dupe(u8, text), null); const result = for (p.panes) |slot| { const pane = slot orelse continue; const file = pane.file orelse continue; if (file.output != null) break file; } else return error.MissingResults; try std.testing.expectEqualStrings(text, result.content); } } test "reference Look selects the next sorted row from the captured request and keeps navigation there" { if (!pardes.lsp.supports.contains(.references)) return error.SkipZigTest; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "m.zig", .data = "const item = 0;\n_ = item;\n_ = item;\n_ = item;\n_ = item;\n" }); try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "a.zig", .data = "a\n" }); try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "z.zig", .data = "z\n" }); var path_buf: [256]u8 = undefined; const path = try std.fmt.bufPrint(&path_buf, ".zig-cache/tmp/{s}/m.zig", .{tmp.sub_path}); const p = try Pardes.init(std.testing.allocator, .{ .file = path, .cols = 80, .rows = 24 }); defer p.deinit(); p.locations_config.layout = .@"inline"; const source = p.panes[0].?; const response = "m.zig:5:5-8 later\nz.zig:1:1 last-file\nm.zig:1:7-10 earlier\nm.zig:3:5-8 current\na.zig:1:1 first-file\nm.zig:4:5-8 next\n"; const sorted = "a.zig:1:1 first-file\nm.zig:1:7-10 earlier\nm.zig:3:5-8 current\nm.zig:4:5-8 next\nm.zig:5:5-8 later\nz.zig:1:1 last-file\n"; source.body.cur_row = 2; source.body.cur_col = 4; p.lspRequest(0, .references, ""); const request = p.lsp_wait.?.id; source.body.cur_row = 4; p.lspResponse(request, response); const rid = source.search_pane orelse return error.MissingResults; const results = p.panes[rid].?; try expectResultRows(sorted, &results.file.?); try std.testing.expectEqual(@as(usize, 0), p.active); try std.testing.expectEqual(@as(i32, 3), source.body.cur_row); try std.testing.expect(source.body.vsel.active); try std.testing.expectEqual(@as(i32, 4), @min(source.body.cur_col, source.body.vsel.col)); try std.testing.expectEqual(@as(i32, 7), @max(source.body.cur_col, source.body.vsel.col)); try std.testing.expectEqual(@as(i32, 3), results.body.cur_row); try std.testing.expectEqual(@as(usize, 3), source.search_row.?); try std.testing.expectEqual(results.serial, p.look_walk_owner.?); try std.testing.expectEqual(@as(i32, 3), results.look_at.?.row); const available = p.freeSlot(); p.update(.{ .key = .{ .cp = 'n' } }); try std.testing.expectEqual(rid, p.active); try std.testing.expectEqual(@as(i32, 4), results.body.cur_row); try std.testing.expectEqual(available, p.freeSlot()); p.update(.{ .key = .{ .cp = 'N' } }); try std.testing.expectEqual(@as(i32, 3), results.body.cur_row); p.active = 0; source.body.cur_row = 4; source.body.cur_col = 4; p.lspRequest(0, .references, ""); p.lspResponse(p.lsp_wait.?.id, response); try std.testing.expect(std.mem.endsWith(u8, p.panes[p.active].?.file.?.path, "/z.zig")); try std.testing.expectEqual(@as(i32, 5), results.body.cur_row); try std.testing.expectEqual(@as(usize, 5), source.search_row.?); const last = p.active; p.lspRequest(last, .references, ""); p.lspResponse(p.lsp_wait.?.id, response); try std.testing.expect(std.mem.endsWith(u8, p.panes[p.active].?.file.?.path, "/a.zig")); const wrapped = p.panes[p.panes[last].?.search_pane.?].?; try std.testing.expectEqual(@as(i32, 0), wrapped.body.cur_row); try std.testing.expectEqual(wrapped.serial, p.look_walk_owner.?); } test "location sorting leaves mixed prose alone and preserves newline shape" { const cases = [_]struct { before: []const u8, after: []const u8, anchor: usize }{ .{ .before = "heading\nz.zig:10:1 z\nnot a location\nx file.zig:2:10 x\nx file.zig:2:2 y\n\n", .after = "heading\nz.zig:10:1 z\nnot a location\nx file.zig:2:10 x\nx file.zig:2:2 y\n\n", .anchor = 1, }, .{ .before = "z.zig:10:1 z\nx file.zig:2:10 x\nx file.zig:2:2 y", .after = "x file.zig:2:2 y\nx file.zig:2:10 x\nz.zig:10:1 z", .anchor = 1, }, }; for (cases) |case| { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); defer p.deinit(); p.locations_config.layout = .@"inline"; const source = try p.setTestFile("notes\n"); try panes.Output.fillResults(p, 0, "/tmp", .search, "", try p.gpa.dupe(u8, case.before), 1); try expectResultRows(case.after, &p.panes[source.search_pane.?].?.file.?); try std.testing.expectEqual(case.anchor, source.search_row.?); } } test "a single language location opens the full spaced filename" { if (!pardes.lsp.supports.contains(.references)) return error.SkipZigTest; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "from.zig", .data = "const item = 0;\n" }); try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "with space.zig", .data = "first\nsecond\n" }); var path_buf: [256]u8 = undefined; const path = try std.fmt.bufPrint(&path_buf, ".zig-cache/tmp/{s}/from.zig", .{tmp.sub_path}); const p = try Pardes.init(std.testing.allocator, .{ .file = path }); defer p.deinit(); p.lspRequest(0, .references, ""); p.lspResponse(p.lsp_wait.?.id, "with space.zig:2:1 second\n"); try std.testing.expect(std.mem.endsWith(u8, p.panes[p.active].?.file.?.path, "/with space.zig")); try std.testing.expectEqual(@as(i32, 1), p.panes[p.active].?.body.cur_row); for (p.panes) |slot| { const pane = slot orelse continue; if (pane.file) |file| try std.testing.expect(file.output == null); } } test "result locations stop before preview text and retain full addresses" { const cases = [_]struct { row: []const u8, path: []const u8, line: usize, col: usize, address: []const u8 }{ .{ .row = "a.zig:2 see other.zig:10:1", .path = "a.zig", .line = 2, .col = 0, .address = "a.zig:2" }, .{ .row = "a.zig:2:10-12 see other.zig:1:1", .path = "a.zig", .line = 2, .col = 10, .address = "a.zig:2:10-12" }, .{ .row = "with space.zig:100:2 café", .path = "with space.zig", .line = 100, .col = 2, .address = "with space.zig:100:2" }, .{ .row = "@p3:20:1 pane", .path = "@p3", .line = 20, .col = 1, .address = "@p3:20:1" }, .{ .row = "/n/peer/file.zig:5-8 span", .path = "/n/peer/file.zig", .line = 5, .col = 0, .address = "/n/peer/file.zig:5-8" }, }; for (cases) |case| { const got = panes.Output.location(case.row); try std.testing.expectEqualStrings(case.path, got.path); try std.testing.expectEqual(case.line, got.at.line); try std.testing.expectEqual(case.col, got.at.col); try std.testing.expectEqualStrings(case.address, case.row[0..got.end]); } } test "reordered identical results preserve the selected row and warm line index" { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); defer p.deinit(); p.locations_config.layout = .@"inline"; const source = try p.setTestFile("notes\n"); const sorted = "a.zig:2:1 first\na.zig:10:1 second\n"; try panes.Output.fillResults(p, 0, "/tmp", .{ .query = .references }, "", try p.gpa.dupe(u8, sorted), null); const result = p.panes[source.search_pane.?].?; const file = &result.file.?; const index = try panes.File.lineIndex(p.gpa, file); const content = file.content.ptr; const revision = file.revision; result.body.cur_row = 1; result.body.cur_col = 0; result.body.vsel = .{ .active = true, .explicit = true, .row = 1, .col = 9 }; result.body.select = true; const selected = result.body.vsel; try panes.Output.fillResults(p, 0, "/tmp", .{ .query = .references }, "new request", try p.gpa.dupe(u8, "a.zig:10:1 second\na.zig:2:1 first\n"), 0); try expectResultRows(sorted, file); try std.testing.expectEqual(content, file.content.ptr); try std.testing.expectEqual(index.ptr, file.line_starts.ptr); try std.testing.expectEqual(revision, file.revision); try std.testing.expectEqual(selected, result.body.vsel); try std.testing.expect(result.body.select); try std.testing.expectEqual(@as(i32, 1), result.body.cur_row); try std.testing.expectEqual(@as(usize, 1), source.search_row.?); } test "result sorting allocation failures preserve the previous answer and navigation" { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); defer p.deinit(); p.locations_config.layout = .@"inline"; const source = try p.setTestFile("notes\n"); try panes.Output.fillResults(p, 0, "/tmp", .{ .query = .references }, "before", try p.gpa.dupe(u8, "a.zig:1:1 keep\n"), 0); const id = source.search_pane.?; const result = p.panes[id].?; const content = result.file.?.content.ptr; const revision = result.file.?.revision; const owner = p.look_walk_owner; var preview: [4096]u8 = @splat('x'); for (0..2) |failure| { var failing = std.testing.FailingAllocator.init(p.gpa, .{ .fail_index = failure }); const original_scratch = p.scratch; p.scratch = .init(failing.allocator()); defer { p.scratch.deinit(); p.scratch = original_scratch; } const incoming = try std.fmt.allocPrint(p.gpa, "z.zig:2:1 {s}\na.zig:1:1 {s}\n", .{ &preview, &preview }); try std.testing.expectError(error.OutOfMemory, panes.Output.fillResults(p, 0, "/tmp", .{ .query = .references }, "after", incoming, 1)); try std.testing.expect(failing.has_induced_failure); try std.testing.expectEqual(content, result.file.?.content.ptr); try std.testing.expectEqual(revision, result.file.?.revision); try std.testing.expectEqualStrings("before", result.file.?.output.?.arg()); try std.testing.expectEqual(id, source.search_pane.?); try std.testing.expectEqual(@as(usize, 0), source.search_row.?); try std.testing.expectEqual(@as(usize, 0), p.active); try std.testing.expectEqual(owner, p.look_walk_owner); } } test "Find sorts whole filenames and next location uses numeric columns" { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); defer p.deinit(); p.locations_config.layout = .@"inline"; const source = try p.setTestFile(""); try panes.Output.fillResults(p, 0, "/tmp", .{ .cmd = .Find }, "", try p.gpa.dupe(u8, "x:2\nx:10\n"), 0); const result = p.panes[source.search_pane.?].?; try std.testing.expectEqualStrings("x:10\nx:2\n", result.file.?.content); try std.testing.expectEqual(@as(usize, 1), source.search_row.?); const rows = "with space.zig:2:2-3 first\nwith space.zig:2:10-12 next\nwith space.zig:10:1 later"; try std.testing.expectEqual(@as(usize, 1), panes.Output.nextResult(rows, &.{}, "with space.zig", .{ .line = 2, .col = 3 })); try std.testing.expectEqual(@as(usize, 2), panes.Output.nextResult(rows, &.{}, "with space.zig", .{ .line = 2, .col = 10 })); try std.testing.expectEqual(@as(usize, 0), panes.Output.nextResult(rows, &.{}, "with space.zig", .{ .line = 10, .col = 1 })); } test "result stepping reaches an unterminated last row" { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); defer p.deinit(); p.locations_config.layout = .@"inline"; const source = try p.setTestFile("one\ntwo\n"); const rows = try std.fmt.allocPrint(p.gpa, "{s}:1:1 first\n{s}:2:1 last", .{ source.file.?.path, source.file.?.path }); try panes.Output.fillResults(p, 0, "/tmp", .search, "", rows, 0); try std.testing.expect(pardes.test_api.searchStep(p, 0, 1)); try std.testing.expectEqual(@as(usize, 1), source.search_row.?); const result = p.panes[source.search_pane.?].?; try std.testing.expectEqual(@as(i32, 1), result.body.cur_row); } test "Mini uses OS source precedence and refreshes one independent snapshot" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "build.zig", .data = "x\n" }); var dir_buf: [4096]u8 = undefined; const dir = dir_buf[0..try tmp.dir.realPath(std.testing.io, &dir_buf)]; const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); defer p.deinit(); try p.panes[0].?.setOwnedCwd(dir); try std.testing.expect(p.executeBuiltinLine(0, "Mini build.zig")); const id = p.active; try std.testing.expect(id != 0); const mini = p.panes[id].?; try std.testing.expect(mini.terminal == null); try std.testing.expectEqualStrings("⠁\n", mini.file.?.content); try std.testing.expect(std.mem.startsWith(u8, mini.file.?.mini.?.source, dir)); const next = p.freeSlot(); const body = mini.file.?.content.ptr; try std.testing.expect(p.executeBuiltinLine(id, "Mini build.zig")); try std.testing.expectEqual(id, p.active); try std.testing.expectEqual(body, mini.file.?.content.ptr); try std.testing.expectEqual(next, p.freeSlot()); try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "build.zig", .data = "xx\n" }); try std.testing.expectEqualStrings("⠁\n", mini.file.?.content); try std.testing.expect(p.executeBuiltinLine(id, "Mini build.zig")); try std.testing.expectEqual(id, p.active); try std.testing.expectEqualStrings("⠉\n", mini.file.?.content); while (p.nextEffect()) |effect| switch (effect) { .watch => |watch| try std.testing.expect(watch.pane != id), else => {}, }; try std.testing.expect(p.executeBuiltinLine(id, "Save")); try std.testing.expect(mini.prompt == .save); } test "Mini dump preserves its exact virtual snapshot and body writes detach colors" { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); defer p.deinit(); const source = try p.setTestFile("pub fn start() void {}\n"); var command_buf: [128]u8 = undefined; const command = try std.fmt.bufPrint(&command_buf, "Mini /virtual/pane/{d}/body", .{source.serial}); try std.testing.expect(p.executeBuiltinLine(0, command)); const id = p.active; const before = p.panes[id].?.file.?; try std.testing.expect(before.mini != null); try pardes.dump.dumpState(p); panes.File.setContent(p, &source.file.?, try p.gpa.dupe(u8, "changed\n")); const restored = try pardes.dump.initFromDump(std.testing.allocator, .{ .tty_only = true }, p.dump_out.?); defer restored.deinit(); const mini = restored.panes[restored.active].?; try std.testing.expectEqualStrings(before.content, mini.file.?.content); try std.testing.expectEqualStrings(before.mini.?.source, mini.file.?.mini.?.source); try std.testing.expectEqualSlices(u8, before.mini.?.colors, mini.file.?.mini.?.colors); try std.testing.expect(before.content.ptr != mini.file.?.content.ptr); while (restored.nextEffect()) |effect| switch (effect) { .watch => |watch| try std.testing.expect(watch.pane != restored.active), else => {}, }; var body_buf: [128]u8 = undefined; const body_path = try std.fmt.bufPrint(&body_buf, "/n/self/pane/{d}/body", .{mini.serial}); const body = try pardes.filesystem.read(restored, body_path); defer restored.gpa.free(body); try std.testing.expectEqualStrings(before.content, body); try pardes.filesystem.write(restored, body_path, "replacement\n"); try std.testing.expectEqualStrings("replacement\n", mini.file.?.content); try std.testing.expect(mini.file.?.mini == null); } test "Mini syntax colors survive toggles themes and rendering without source access" { if (!pardes.syntax.enabled) return error.SkipZigTest; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "demo.zig", .data = "pub fn main() void {\n if (true) return;\n}\n" }); var dir_buf: [4096]u8 = undefined; const dir = dir_buf[0..try tmp.dir.realPath(std.testing.io, &dir_buf)]; const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 24 }); defer p.deinit(); try p.panes[0].?.setOwnedCwd(dir); p.settings.colors = false; try panes.Mini.open(p, 0, "demo.zig"); const id = p.active; const mini = p.panes[id].?; const keyword = std.mem.indexOfScalar(u8, mini.file.?.mini.?.colors, @intFromEnum(pardes.syntax.Syn.keyword)) orelse return error.MissingKeywordColor; // Direct pane construction bypasses the host update's layout sync. p.update(.{ .resize = .{ .cols = 80, .rows = 24 } }); const position = modal.positionAt(mini.file.?.content, keyword); const column = panes.File.displayWidth(modal.lineSlice(mini.file.?.content, position.row)[0..position.col]); try tmp.dir.deleteFile(std.testing.io, "demo.zig"); const colors = mini.file.?.mini.?.colors.ptr; var frame = std.heap.ArenaAllocator.init(std.testing.allocator); defer frame.deinit(); for ([_]bool{ false, true, false, true }) |enabled| { p.settings.colors = enabled; mini.file.?.syntax_dirty = true; const surface = try p.render(frame.allocator()); const rect = p.rects[id]; const x = rect.x + config.GUTTER + panes.File.gutterWidth(mini) + @as(u16, @intCast(column)); const y = p.bodyTop(mini, rect) + @as(u16, @intCast(position.row)); if (enabled) try std.testing.expectEqualDeep(pardes.Color{ .rgb = p.theme().kw }, surface.at(x, y).style.fg); try std.testing.expectEqual(colors, mini.file.?.mini.?.colors.ptr); try std.testing.expectEqual(@as(usize, 0), mini.file.?.highlights.len); _ = frame.reset(.retain_capacity); try std.testing.expect(p.executeBuiltinLine(id, "NextColor")); } } test "Mini restore rejects inconsistent metadata before publishing a pane" { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); defer p.deinit(); const id = p.freeSlot().?; const serial = p.next_serial; for ([_]pardes.dump.File{ .{ .content = "x", .mini_source = "/source.zig", .mini_colors_b64 = "AA==" }, .{ .content = "x", .origin = "Mini", .mini_source = "/source.zig", .mini_colors_b64 = "" }, .{ .content = "x", .origin = "Mini", .mini_source = "/source.zig", .mini_colors_b64 = "BQ==" }, .{ .content = "x", .origin = "Mini", .mini_colors_b64 = "AA==" }, }) |saved| { try std.testing.expectError(error.InvalidMini, panes.File.restore(p, id, .{ .kind = .file, .tag = "", .body = "", .file = saved })); try std.testing.expect(p.panes[id] == null); try std.testing.expectEqual(serial, p.next_serial); } } test "Mini failures leave its existing snapshot and focus unchanged" { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); defer p.deinit(); const source = try p.setTestFile("abc\n"); var path_buf: [128]u8 = undefined; const path = try std.fmt.bufPrint(&path_buf, "/virtual/pane/{d}/body", .{source.serial}); try panes.Mini.open(p, 0, path); const id = p.active; const mini = p.panes[id].?; const body = mini.file.?.content.ptr; const next = p.freeSlot(); try std.testing.expectError(error.FileNotFound, panes.Mini.open(p, 0, "/virtual/does-not-exist")); try std.testing.expectEqual(id, p.active); try std.testing.expectEqual(body, mini.file.?.content.ptr); try std.testing.expectEqual(next, p.freeSlot()); } test "Config prints the startup path and refreshes its one output" { const path = "/home/pardes-test/.config/pardes/init"; const p = try Pardes.init(std.testing.allocator, .{ .startup_config_path = path }); defer p.deinit(); try std.testing.expect(p.executeBuiltinLine(0, "Config")); const opened = p.active; const out = p.panes[opened].?.file.?; for ([_][]const u8{ "Startup config: " ++ path ++ "\n", "Theme orchard\n", "Shell effective (last spawn) (none)\n", "Shell pending on\n", }) |line| try std.testing.expect(std.mem.indexOf(u8, out.content, line) != null); var default_line: [256]u8 = undefined; try std.testing.expect(std.mem.indexOf(u8, out.content, try std.fmt.bufPrint(&default_line, "Shell requested (new panes) {s} (default)\n", .{config.defaultShell()})) != null); try std.testing.expectEqualStrings(config.config_buffer, std.fs.path.basename(out.path)); try std.testing.expectEqual(panes.Output.Origin{ .cmd = .Config }, out.output.?.from); try std.testing.expect(p.executeBuiltinLine(0, "Config")); try std.testing.expectEqual(opened, p.active); var buffers: usize = 0; for (p.panes) |slot| { const f = (slot orelse continue).file orelse continue; const origin = (f.output orelse continue).from; buffers += @intFromBool(std.meta.eql(origin, panes.Output.Origin{ .cmd = .Config })); } try std.testing.expectEqual(@as(usize, 1), buffers); } test "Config reports the absence of a per-user config path" { const p = try Pardes.init(std.testing.allocator, .{}); defer p.deinit(); try std.testing.expect(p.executeBuiltinLine(0, "Config")); const report = p.panes[p.active].?.file.?.content; try std.testing.expect(std.mem.indexOf(u8, report, "no per-user config path") != null); } test "EffectCode lists readable virtual sources used by this backend" { if (comptime !builtins.EffectCode.enabled) return; const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); defer p.deinit(); if (!pardes.memory.limits.embedded_sources) { const before = p.active; try std.testing.expect(p.executeBuiltinLine(0, "EffectCode PanelSlide")); try std.testing.expectEqual(before, p.active); return error.SkipZigTest; } try std.testing.expect(p.executeBuiltinLine(0, "EffectCode PanelSlide")); const out = p.panes[p.active].?.file.?; try std.testing.expectEqualStrings(config.effect_code_buffer, std.fs.path.basename(out.path)); try std.testing.expectEqual(panes.Output.Origin{ .cmd = .EffectCode }, out.output.?.from); try std.testing.expectEqualStrings("PanelSlide", out.output.?.arg()); try std.testing.expect(out.content.len < 1024); try std.testing.expect(std.mem.indexOf(u8, out.content, "/virtual/src/layout.zig\n") != null); const backend_source = switch (platform) { .tty => "/virtual/src/tty/panel_compositor.zig", .gui => if (@import("pardes_config").gui_shader_sources_prebuilt) "/virtual/shaders/prebuilt/ui.vert.glsl" else "/virtual/shaders/ui.vert.glsl", .macos => "/virtual/src/macos/Sources/ScenePostprocessor.swift", .web, .esp32p4 => unreachable, }; try std.testing.expect(std.mem.indexOf(u8, out.content, backend_source) != null); var paths = std.mem.tokenizeScalar(u8, out.content, '\n'); var count: usize = 0; while (paths.next()) |path| { if (!std.mem.startsWith(u8, path, "/virtual/")) continue; const expected = pardes.filesystem.sourceBytes(path["/virtual/".len..]) orelse return error.MissingEffectSource; const bytes = try pardes.filesystem.read(p, path); defer p.gpa.free(bytes); try std.testing.expectEqualStrings(expected, bytes); count += 1; } try std.testing.expect(count >= 3); const report = p.active; pardes.look.lookAt(p, report, backend_source); try std.testing.expect(p.active != report); const source = p.panes[p.active].?.file.?; try std.testing.expectEqualStrings(backend_source, source.path); try std.testing.expectEqualStrings(pardes.filesystem.sourceBytes(backend_source["/virtual/".len..]).?, source.content); } test "every enabled setting builtin mutates the State Config reports" { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); defer p.deinit(); p.locations_config.layout = .@"inline"; try std.testing.expectEqual(config.gui_tagline_font_percent, p.settings.font.tagline_percent); var font_arena: std.heap.ArenaAllocator = .init(std.testing.allocator); defer font_arena.deinit(); var chosen_font: ?[]const u8 = null; var buf: [512]u8 = undefined; for (config.Runtime.settings) |setting| { if (!setting.enabled(builtins.capabilities)) continue; const command: []const u8 = switch (setting.action) { .theme => try std.fmt.bufPrint(&buf, "{s} acme", .{setting.word}), .shell => try std.fmt.bufPrint(&buf, "{s} fish", .{setting.word}), .tagline_size => try std.fmt.bufPrint(&buf, "{s} 67", .{setting.word}), .font => continue, else => setting.word, }; try std.testing.expect(p.executeBuiltinLine(p.active, command)); } if (comptime font_picker) { const before_invalid = p.settings.font.tagline_percent; try std.testing.expect(p.executeBuiltinLine(p.active, "TaglineSize 0")); try std.testing.expectEqual(before_invalid, p.settings.font.tagline_percent); const installed = fonts.list(font_arena.allocator(), null); if (installed.len > 0) { chosen_font = installed[0].name; const command = try std.fmt.bufPrint(&buf, "Font {s}", .{installed[0].name}); try std.testing.expect(p.executeBuiltinLine(p.active, command)); } } try std.testing.expect(p.executeBuiltinLine(p.active, "Config")); const report = p.panes[p.active].?.file.?.content; for ([_][]const u8{ "Colors off\n", "Wrap off\n", "Tagbottom on\n", "Debug on\n", "Theme acme\n", "Shell requested (new panes) fish\n", }) |line| try std.testing.expect(std.mem.indexOf(u8, report, line) != null); const transition = if (builtins.capabilities.panel_transitions) try std.fmt.bufPrint(&buf, "Panel transition {s}\n", .{ config.Runtime.findAction(.{ .transition = p.settings.panel_transition }).?.word, }) else "Panel transition unsupported\n"; try std.testing.expect(std.mem.indexOf(u8, report, transition) != null); // A bare word turns a bundled pass on at its default level. const scene_status = if (builtins.capabilities.scene_shaders) "2" else "unsupported"; for ([_][]const u8{"Crt"}) |name| { const line = try std.fmt.bufPrint(&buf, "{s} {s}\n", .{ name, scene_status }); try std.testing.expect(std.mem.indexOf(u8, report, line) != null); } const tagline = if (!builtins.capabilities.tagline_font_size) "TaglineSize unsupported\n" else try std.fmt.bufPrint(&buf, "TaglineSize {d}{s}\n", .{ p.settings.font.tagline_percent, if (builtins.capabilities.font_picker) "" else " (build-time only)", }); try std.testing.expect(std.mem.indexOf(u8, report, tagline) != null); if (chosen_font) |name| { try std.testing.expect(std.mem.indexOf(u8, report, name) != null); try std.testing.expect(std.mem.indexOf(u8, report, "Font pending on\n") != null); } } fn walkFixture(p: *Pardes, id: usize, cwd: []const u8, pattern: []const u8) !usize { const rows = try p.gpa.dupe(u8, \\build.zig:1:1 first \\(mise.toml) and build.zig.zon:3:2-9 two on one row \\nothing look-able on this row at all \\uucode_config.zig:7:1 last \\ ); try panes.Output.fillResults(p, id, cwd, .search, pattern, rows, null); return p.panes[id].?.search_pane orelse error.MissingResults; } const ProjectPaths = struct { cwd: []const u8, boot: []const u8 }; fn projectPaths(cwd_buf: *[4096]u8, path_buf: *[4096]u8) !ProjectPaths { const raw = std.c.getcwd(cwd_buf, cwd_buf.len) orelse return error.GetCwdFailed; const cwd = std.mem.span(@as([*:0]u8, @ptrCast(raw))); return .{ .cwd = cwd, .boot = try std.fmt.bufPrint(path_buf, "{s}/mise.toml", .{cwd}) }; } fn selectedOutputText(pane: *const pardes.Pane) ?[]const u8 { const file = pane.file orelse return null; if (!pane.body.vsel.active or pane.body.vsel.row != pane.body.cur_row or pane.body.cur_row < 0) return null; const line = modal.lineSlice(file.content, @intCast(pane.body.cur_row)); const lo: usize = @intCast(@max(0, @min(pane.body.vsel.col, pane.body.cur_col))); const hi: usize = @intCast(@max(0, @max(pane.body.vsel.col, pane.body.cur_col))); if (lo >= line.len) return ""; return line[lo..@min(line.len, hi + 1)]; } test "n/N in ordinary text skips comment slashes and retains real paths" { if (platform == .web) return error.SkipZigTest; const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 20 }); defer p.deinit(); const pane = try p.setTestFile("// ordinary comment\n/// documentation comment\n(//) [///] punctuation\n/\n// /tmp\n//tmp\n"); try std.testing.expect(pane.file.?.output == null); pardes.look.armLookWalk(p, 0); const available = p.freeSlot(); for ([_]struct { key: u21, row: i32, text: []const u8 }{ .{ .key = 'n', .row = 3, .text = "/" }, .{ .key = 'n', .row = 4, .text = "/tmp" }, .{ .key = 'n', .row = 5, .text = "//tmp" }, .{ .key = 'n', .row = 3, .text = "/" }, .{ .key = 'N', .row = 5, .text = "//tmp" }, .{ .key = 'N', .row = 4, .text = "/tmp" }, .{ .key = 'N', .row = 3, .text = "/" }, }) |step| { p.update(.{ .key = .{ .cp = step.key } }); try std.testing.expectEqual(@as(usize, 0), p.active); try std.testing.expectEqual(step.row, pane.body.cur_row); try std.testing.expectEqualStrings(step.text, selectedOutputText(pane) orelse ""); try std.testing.expectEqual(available, p.freeSlot()); } } test "n/N skips formatted context and selects comment match locations" { if (!pardes.syntax.enabled) return error.SkipZigTest; for ([_]panes.Output.Origin{ .search, .{ .query = .references } }) |origin| { for ([_]bool{ false, true }) |tslocations| { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 30 }); defer p.deinit(); p.locations_config.layout = .@"inline"; const text = "const Box = struct {\n" ++ " pub fn run() void {\n" ++ " // /tmp neighboring path\n" ++ " // actual match /tmp\n" ++ " // / another neighbor\n" ++ " call();\n" ++ " // /tmp trailing context\n" ++ " }\n};\n"; const source = try p.setTestFile(text); p.gpa.free(source.file.?.path); source.file.?.path = try p.gpa.dupe(u8, "/look-walk-context.zig"); p.presentation.enabled = false; p.locations_config = .{ .layout = .@"inline", .context = 1, .tscontext = true, .tslocations = tslocations }; const path = source.file.?.path; const input = try std.fmt.allocPrint(p.gpa, "{s}:4:9 {s}\n{s}:6:9 {s}\n", .{ path, modal.lineSlice(text, 3), path, modal.lineSlice(text, 5), }); try panes.Output.fillResults(p, 0, "/", origin, "walk", input, null); const id = source.search_pane.?; const result = p.panes[id].?; const file = &result.file.?; var matches: [2]usize = undefined; var count: usize = 0; for (file.location_rows, 0..) |row, index| { if (row.kind != .match) continue; matches[count] = index; count += 1; } try std.testing.expectEqual(@as(usize, 2), count); const available = p.freeSlot(); for ([_]struct { key: u21, match: usize }{ .{ .key = 'n', .match = 0 }, .{ .key = 'n', .match = 1 }, .{ .key = 'n', .match = 0 }, .{ .key = 'N', .match = 1 }, .{ .key = 'N', .match = 0 }, }) |step| { p.update(.{ .key = .{ .cp = step.key } }); try std.testing.expectEqual(id, p.active); const row = matches[step.match]; try std.testing.expectEqual(@as(i32, @intCast(row)), result.body.cur_row); const line = modal.lineSlice(file.content, row); try std.testing.expectEqualStrings(line[0..file.location_rows[row].location_end], selectedOutputText(result) orelse ""); try std.testing.expectEqual(available, p.freeSlot()); } try pardes.dump.dumpState(p); const restored = try pardes.dump.initFromDump(p.gpa, .{ .tty_only = true }, p.dump_out.?); defer restored.deinit(); pardes.look.armLookWalk(restored, id); const restored_result = restored.panes[id].?; // The dot came back on the match it was on (the first), so the // walk goes on from there. try std.testing.expectEqual(@as(i32, @intCast(matches[0])), restored_result.body.cur_row); restored.update(.{ .key = .{ .cp = 'n' } }); try std.testing.expectEqual(@as(i32, @intCast(matches[1])), restored_result.body.cur_row); restored.update(.{ .key = .{ .cp = 'N' } }); try std.testing.expectEqual(@as(i32, @intCast(matches[0])), restored_result.body.cur_row); const restored_file = &restored_result.file.?; const last = modal.lineSlice(restored_file.content, matches[0]); try std.testing.expectEqualStrings(last[0..restored_file.location_rows[matches[0]].location_end], selectedOutputText(restored_result) orelse ""); } } } test "n/N selects one output location per row and opens nothing" { if (platform == .web) return; var cwd_buf: [4096]u8 = undefined; var path_buf: [4096]u8 = undefined; const paths = try projectPaths(&cwd_buf, &path_buf); const p = try Pardes.init(std.testing.allocator, .{ .cols = 100, .rows = 40, .file = paths.boot }); defer p.deinit(); p.locations_config.layout = .@"inline"; p.update(.{ .resize = .{ .cols = 100, .rows = 40 } }); const rid = try walkFixture(p, p.active, paths.cwd, "one"); const results = p.panes[rid].?; const panes_before = p.freeSlot(); p.update(.{ .key = .{ .cp = 'n' } }); try std.testing.expectEqual(rid, p.active); try std.testing.expectEqual(panes_before, p.freeSlot()); try std.testing.expect(results.body.vsel.active and results.body.vsel.explicit); try std.testing.expectEqualStrings("build.zig:1:1", selectedOutputText(results) orelse ""); try std.testing.expectEqual(panes.Output.Grain.line, panes.Output.grain(results.file.?.output)); p.update(.{ .key = .{ .cp = 'n' } }); try std.testing.expectEqual(@as(i32, 1), results.body.cur_row); try std.testing.expectEqualStrings("mise.toml", selectedOutputText(results) orelse ""); p.update(.{ .key = .{ .cp = 'n' } }); try std.testing.expectEqual(@as(i32, 3), results.body.cur_row); // row 2 is not look-able try std.testing.expectEqualStrings("uucode_config.zig:7:1", selectedOutputText(results) orelse ""); p.update(.{ .key = .{ .cp = 'n' } }); try std.testing.expectEqual(@as(i32, 0), results.body.cur_row); // ring seam p.update(.{ .key = .{ .cp = Key.enter } }); try std.testing.expect(p.freeSlot() != panes_before); try std.testing.expect(std.mem.endsWith(u8, p.panes[p.active].?.file.?.path, "/build.zig")); try std.testing.expectEqual(panes.Output.Grain.word, panes.Output.grain(p.panes[p.active].?.file.?.output)); } test "n/N resumes the result output whose Look moved focus away" { if (platform == .web) return; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "look-owner.txt", .data = "alpha target\nbeta target\ngamma target\n", }); var path_buf: [256]u8 = undefined; const path = try std.fmt.bufPrint(&path_buf, ".zig-cache/tmp/{s}/look-owner.txt", .{tmp.sub_path}); const p = try Pardes.init(std.testing.allocator, .{ .file = path, .cols = 80, .rows = 24 }); defer p.deinit(); p.locations_config.layout = .@"inline"; p.settings.look_word = .list; // the +Search listing is what is walked try std.testing.expect(p.executeBuiltinLine(0, "Look target")); const rid = p.panes[0].?.search_pane orelse return error.MissingResults; const results = p.panes[rid].?; const first = results.body.cur_row; try std.testing.expectEqual(@as(usize, 0), p.active); try std.testing.expect(results.look_at != null); try std.testing.expectEqual(results.serial, p.look_walk_owner orelse return error.MissingLookOwner); p.update(.{ .key = .{ .cp = 'n' } }); try std.testing.expectEqual(rid, p.active); try std.testing.expectEqual(first + 1, results.body.cur_row); p.update(.{ .key = .{ .cp = 'N' } }); try std.testing.expectEqual(first, results.body.cur_row); // A later no-match answer has no position to resume and therefore cannot // steal the provenance established by the Look above. const owner = results.serial; const dir = std.fs.path.dirname(path) orelse "."; try panes.Output.fillResults( p, 0, dir, .search, "no-such-result", try p.gpa.dupe(u8, ""), null, ); try std.testing.expectEqual(owner, p.look_walk_owner.?); p.update(.{ .key = .{ .cp = 'n' } }); try std.testing.expectEqual(rid, p.active); try std.testing.expectEqual(first + 1, results.body.cur_row); // Emptying the owner itself keeps its identity as the ring's starting // point. Deleting it then leaves a stale serial, never an id that can bind // to the unrelated pane subsequently allocated in the same slot. try panes.Output.fillResults(p, 0, dir, .search, "target", try p.gpa.dupe(u8, ""), null); try std.testing.expectEqual(owner, p.look_walk_owner.?); try std.testing.expect(p.executeBuiltinLine(rid, "Del")); try std.testing.expect(p.paneBySerial(owner) == null); const replacement = try p.newShell(rid, ""); try std.testing.expect(replacement.serial != owner); try std.testing.expect(p.paneBySerial(owner) == null); } test "N exactly reverses n across output panes and the ring seam" { if (platform == .web) return; var cwd_buf: [4096]u8 = undefined; var path_buf: [4096]u8 = undefined; const paths = try projectPaths(&cwd_buf, &path_buf); const p = try Pardes.init(std.testing.allocator, .{ .cols = 100, .rows = 40, .file = paths.boot }); defer p.deinit(); p.locations_config.layout = .@"inline"; p.update(.{ .resize = .{ .cols = 100, .rows = 40 } }); const first = try walkFixture(p, p.active, paths.cwd, "one"); p.update(.{ .key = .{ .cp = 'n', .alt = true } }); const second = try walkFixture(p, p.active, paths.cwd, "two"); try std.testing.expect(first != second); const Mark = struct { pane: usize, row: i32, col: i32 }; const here = struct { fn at(pp: *Pardes) Mark { const pane = pp.panes[pp.active].?; return .{ .pane = pp.active, .row = pane.body.cur_row, .col = pane.body.cur_col }; } }.at; p.update(.{ .key = .{ .cp = 'n' } }); const base = here(p); var trail: [12]Mark = undefined; for (&trail) |*mark| { p.update(.{ .key = .{ .cp = 'n' } }); mark.* = here(p); } var i = trail.len; while (i > 0) { i -= 1; p.update(.{ .key = .{ .cp = 'N' } }); const want = if (i == 0) base else trail[i - 1]; try std.testing.expectEqual(want, here(p)); } var saw_first = false; var saw_second = false; for (trail) |mark| { saw_first = saw_first or mark.pane == first; saw_second = saw_second or mark.pane == second; } try std.testing.expect(saw_first and saw_second); } test "Themes lists the ring in sections and skips headings in both directions" { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 40 }); defer p.deinit(); const source = try p.setTestFile("notes\n"); pardes.colors.setThemeIndex(p, pardes.themes.len - 1); const original_theme = p.settings.theme; try std.testing.expect(p.executeBuiltinLine(0, "Themes")); const tid = source.search_pane orelse return error.MissingThemeList; const list = p.panes[tid].?; const content = list.file.?.content; try std.testing.expectEqual(@as(usize, 0), p.active); try std.testing.expectEqual(@as(i32, 0), list.body.cur_row); try std.testing.expectEqual(@as(usize, 0), list.file.?.scroll); try std.testing.expectEqualStrings("# Pardes themes", modal.lineSlice(content, 0)); // Every theme in ring order, each section's heading after a blank line // where colors.groupAt says one starts. var row: usize = 1; var sections: usize = 1; var first_break: ?usize = null; for (pardes.themes, 0..) |theme, i| { if (i > 0) if (pardes.colors.groupAt(i)) |title| { try std.testing.expectEqualStrings("", modal.lineSlice(content, row)); try std.testing.expect(std.mem.startsWith(u8, modal.lineSlice(content, row + 1), "# ")); try std.testing.expectEqualStrings(title, modal.lineSlice(content, row + 1)["# ".len..]); row += 2; sections += 1; if (first_break == null) first_break = i; }; const line = modal.lineSlice(content, row); try std.testing.expect(std.mem.startsWith(u8, line, "Theme ")); try std.testing.expectEqualStrings(theme.name, line["Theme ".len..]); row += 1; } try std.testing.expectEqual(pardes.themes.len + 1 + 2 * (sections - 1), std.mem.count(u8, content, "\n")); // acme is a current theme: it is listed before any break. const acme = for (pardes.themes, 0..) |t, i| { if (std.mem.eql(u8, t.name, "acme")) break i; } else return error.MissingAcme; const brk = first_break orelse return error.NoSections; try std.testing.expect(acme < brk); // Opening an imported theme's selector must still begin with the natives. // Walk across the section break without selecting or executing a heading. for (pardes.themes[0 .. brk + 1]) |theme| { p.update(.{ .key = .{ .cp = 'n' } }); try std.testing.expectEqual(tid, p.active); const selected = selectedOutputText(list) orelse return error.MissingThemeSection; try std.testing.expectEqualStrings(theme.name, selected["Theme ".len..]); try std.testing.expectEqual(original_theme, p.settings.theme); } p.update(.{ .key = .{ .cp = 'N' } }); const previous = selectedOutputText(list) orelse return error.MissingThemeSection; try std.testing.expectEqualStrings(pardes.themes[brk - 1].name, previous["Theme ".len..]); p.update(.{ .key = .{ .cp = Key.tab } }); try std.testing.expectEqual(brk - 1, p.settings.theme); } test "n/N selects command outputs by whole row" { if (platform == .web) return; var cwd_buf: [4096]u8 = undefined; var path_buf: [4096]u8 = undefined; const paths = try projectPaths(&cwd_buf, &path_buf); const p = try Pardes.init(std.testing.allocator, .{ .cols = 100, .rows = 40, .file = paths.boot }); defer p.deinit(); p.update(.{ .resize = .{ .cols = 100, .rows = 40 } }); try std.testing.expect(p.executeBuiltinLine(p.active, "Themes")); const tid = p.panes[p.active].?.search_pane orelse return error.MissingThemeList; const themes = p.panes[tid].?; try std.testing.expect(panes.Output.fileTraits(themes.file.?.output).commands); p.update(.{ .key = .{ .cp = 'n' } }); const row0 = std.mem.trimEnd(u8, modal.lineSlice(themes.file.?.content, 1), " \t\r"); try std.testing.expectEqualStrings(row0, selectedOutputText(themes) orelse ""); const theme_before = p.settings.theme; p.update(.{ .key = .{ .cp = 'n' } }); p.update(.{ .key = .{ .cp = Key.tab } }); try std.testing.expect(p.settings.theme != theme_before); try std.testing.expect(p.executeBuiltinLine(0, "Look build.zig")); p.update(.tick); try std.testing.expect(p.active != 0); p.update(.{ .key = .{ .cp = 'n' } }); try std.testing.expectEqual(@as(usize, 0), p.active); } test "identical result refresh keeps warm content caches without allocation" { var allocator: std.testing.FailingAllocator = .init(std.testing.allocator, .{}); const p = try Pardes.init(allocator.allocator(), .{ .tty_only = true, .cols = 80, .rows = 24 }); defer p.deinit(); p.locations_config.layout = .@"inline"; const source = try p.setTestFile("notes\n"); p.settings.colors = true; const text = "a.zig:1:1 pub fn first() void {}\na.zig:2:1 if (true) return;\n"; try panes.Output.fillResults(p, 0, "/tmp", .{ .query = .references }, "before", try p.gpa.dupe(u8, text), 0); const id = source.search_pane orelse return error.MissingResults; const result = p.panes[id].?; const file = &result.file.?; result.body.cur_row = 1; result.body.cur_col = 12; result.body.vsel = .{ .active = true, .explicit = true, .row = 0, .col = 2 }; result.body.select = true; file.scroll = 1; result.hscroll = 3; panes.File.refreshHighlights(p); const index = try panes.File.lineIndex(p.gpa, file); const styles = try std.testing.allocator.dupe(u8, file.highlights); defer std.testing.allocator.free(styles); if (@import("pardes_config").syntax_zig_grammar) try std.testing.expect(styles.len > 0); const revision = file.revision; const saved_revision = file.saved_revision; const body_ptr = file.content.ptr; const style_ptr = file.highlights.ptr; const highlight_start = file.highlight_start; const selected = result.body.vsel; const event_node = pardes.ctlfs.Node.of(result.serial, .event); const opened = pardes.ctlfs.handle(p, .{ .tag = 0, .op = .open, .node = event_node }); try std.testing.expectEqual(.ok, opened.status); defer _ = pardes.ctlfs.handle(p, .{ .tag = 0, .op = .release, .node = event_node, .handle = opened.handle }); for (0..32) |_| { const incoming = try p.gpa.dupe(u8, text); const allocations = allocator.allocations; const freed = allocator.freed_bytes; const deallocations = allocator.deallocations; allocator.fail_index = allocator.alloc_index; defer allocator.fail_index = std.math.maxInt(usize); p.active = id; source.search_pane = null; source.search_row = null; p.look_walk_owner = null; try panes.Output.fillResults(p, 0, "/tmp", .{ .query = .references }, "after", incoming, 1); try std.testing.expectEqual(revision, file.revision); try std.testing.expectEqual(saved_revision, file.saved_revision); try std.testing.expectEqual(body_ptr, file.content.ptr); try std.testing.expectEqual(index.ptr, file.line_starts.ptr); try std.testing.expectEqual(style_ptr, file.highlights.ptr); try std.testing.expectEqual(highlight_start, file.highlight_start); try std.testing.expectEqualSlices(u8, styles, file.highlights); try std.testing.expect(!file.syntax_dirty); panes.File.refreshHighlights(p); try std.testing.expectEqual(allocations, allocator.allocations); try std.testing.expectEqual(freed + text.len, allocator.freed_bytes); try std.testing.expectEqual(deallocations + 1, allocator.deallocations); try std.testing.expect(!allocator.has_induced_failure); try std.testing.expectEqual(selected, result.body.vsel); try std.testing.expect(result.body.select); try std.testing.expectEqual(@as(i32, 1), result.body.cur_row); try std.testing.expectEqual(@as(i32, 12), result.body.cur_col); try std.testing.expectEqual(@as(usize, 1), file.scroll); try std.testing.expectEqual(@as(i32, 3), result.hscroll); try std.testing.expectEqualStrings("after", file.output.?.arg()); try std.testing.expectEqual(@as(usize, 0), p.active); try std.testing.expectEqual(id, source.search_pane.?); try std.testing.expectEqual(@as(usize, 1), source.search_row.?); try std.testing.expectEqual(result.serial, p.look_walk_owner.?); try std.testing.expect(p.panes[id].?.fs.events.empty()); } try panes.Output.fillResults(p, 0, "/tmp", .{ .query = .references }, "changed", try p.gpa.dupe(u8, "short\n"), null); try std.testing.expectEqual(revision + 1, file.revision); try std.testing.expectEqualStrings("short\n", file.content); try std.testing.expectEqualStrings("changed", file.output.?.arg()); // The warm index is carried through the edit, not dropped. try std.testing.expectEqualSlices(usize, &.{ 0, 6 }, file.line_starts); try std.testing.expectEqual(@as(usize, 0), file.highlights.len); try std.testing.expect(file.syntax_dirty); try std.testing.expect(!result.body.vsel.active and !result.body.select); try std.testing.expectEqual(@as(i32, 0), result.body.cur_row); try std.testing.expectEqual(@as(i32, 0), result.body.cur_col); try std.testing.expectEqual(@as(usize, 0), file.scroll); try std.testing.expectEqual(@as(i32, 0), result.hscroll); try std.testing.expect(source.search_row == null); const events = &p.panes[id].?.fs.events; try std.testing.expect(std.mem.startsWith(u8, events.peek().?, "KD")); events.pop(); try std.testing.expect(std.mem.startsWith(u8, events.peek().?, "KI")); events.pop(); try std.testing.expect(events.empty()); } test "rejected result refresh frees incoming text and leaves the existing output intact" { var allocator: std.testing.FailingAllocator = .init(std.testing.allocator, .{}); const p = try Pardes.init(allocator.allocator(), .{ .tty_only = true, .cols = 80, .rows = 24 }); defer p.deinit(); p.locations_config.layout = .@"inline"; const source = try p.setTestFile("notes\n"); try panes.Output.fillResults(p, 0, "/tmp", .{ .query = .references }, "before", try p.gpa.dupe(u8, "keep\n"), 1); const id = source.search_pane.?; const result = p.panes[id].?; const file = &result.file.?; const body_ptr = file.content.ptr; const revision = file.revision; const free_slot = p.freeSlot(); var oversized: [panes.Output.max_arg + 1]u8 = @splat('x'); for ([_]bool{ false, true }) |oom| { const incoming = try p.gpa.dupe(u8, "discard\n"); const freed = allocator.freed_bytes; const deallocations = allocator.deallocations; if (oom) allocator.fail_index = allocator.alloc_index; defer allocator.fail_index = std.math.maxInt(usize); try std.testing.expectError(if (oom) error.OutOfMemory else error.ArgumentTooLong, panes.Output.fillResults( p, 0, "/tmp", if (oom) .search else .{ .query = .references }, if (oom) "new output" else &oversized, incoming, 0, )); try std.testing.expectEqual(freed + "discard\n".len, allocator.freed_bytes); try std.testing.expectEqual(deallocations + 1, allocator.deallocations); try std.testing.expectEqual(body_ptr, file.content.ptr); try std.testing.expectEqual(revision, file.revision); try std.testing.expectEqualStrings("keep\n", file.content); try std.testing.expectEqualStrings("before", file.output.?.arg()); try std.testing.expectEqual(free_slot, p.freeSlot()); try std.testing.expectEqual(id, source.search_pane.?); try std.testing.expectEqual(@as(usize, 1), source.search_row.?); try std.testing.expectEqual(@as(usize, 0), p.active); } try std.testing.expect(allocator.has_induced_failure); } test "Look ignores slash-only comment markers while retaining root and real paths" { const look = pardes.look; var realbuf: [4096]u8 = undefined; for ([_][]const u8{ "//", "///", "////", "//:12" }) |marker| { try std.testing.expect(look.resolve(null, marker, ".", &realbuf) == .none); try std.testing.expectEqual(@as(?look.Span, null), look.wordSpan(null, marker, ".", &realbuf)); } for ([_][]const u8{ "(//),", "///!" }) |marker| try std.testing.expectEqual(@as(?look.Span, null), look.wordSpan(null, marker, ".", &realbuf)); try std.testing.expectEqual(@as(?look.Span, null), look.lineSpan(null, " // ordinary comment", ".", &realbuf)); if (platform == .web) return; try std.testing.expect(look.resolve(null, "/", ".", &realbuf) == .dir); try std.testing.expect(look.wordSpan(null, "/", ".", &realbuf) != null); var pathbuf: [4096]u8 = undefined; const cwd = pathbuf[0..try std.process.currentPath(std.testing.io, &pathbuf)]; const doubled = try std.fmt.allocPrint(std.testing.allocator, "/{s}", .{cwd}); defer std.testing.allocator.free(doubled); try std.testing.expect(look.resolve(null, doubled, ".", &realbuf) == .dir); try std.testing.expect(look.resolve(null, "https://example.com/path", ".", &realbuf) == .url); } test "result alignment chunks keep seventeen matches independent and refresh stable" { for ([_]panes.Output.Origin{ .search, .{ .query = .references } }) |origin| { for ([_]usize{ 7, 8 }) |long_at| { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); defer p.deinit(); p.locations_config.layout = .@"inline"; const source = try p.setTestFile("notes\n"); p.locations_config = .{ .layout = .@"inline", .context = 0, .tscontext = false }; var input: std.ArrayList(u8) = .empty; defer input.deinit(p.gpa); var widths: [17]usize = undefined; for (0..17) |reverse| { const index = 16 - reverse; const label = try std.fmt.allocPrint(p.gpa, "/r{d:0>2}{s}.zig:3:1", .{ index, if (index == long_at) "-界-very-long-path-name" else "-é" }); defer p.gpa.free(label); widths[index] = panes.File.displayWidth(label); try input.appendSlice(p.gpa, label); try input.appendSlice(p.gpa, " \t return 7;\n"); } try panes.Output.fillResults(p, 0, "/", origin, "chunks", try p.gpa.dupe(u8, input.items), 0); const result_id = source.search_pane.?; const result = p.panes[result_id].?; const file = &result.file.?; try std.testing.expectEqual(@as(usize, 17), file.location_rows.len); try std.testing.expectEqual(@as(usize, 16), source.search_row.?); var starts: [17]usize = undefined; for (file.location_rows, 0..) |row, index| { const line = modal.lineSlice(file.content, index); const first = index / 8 * 8; var width: usize = 0; for (widths[first..@min(first + 8, widths.len)]) |label_width| width = @max(width, label_width); try std.testing.expectEqual(width, panes.File.displayWidth(line[0 .. row.code_start - 1])); try std.testing.expectEqual(@as(u8, '\t'), line[row.code_start - 1]); try std.testing.expectEqualStrings("\t return 7;", line[row.code_start..]); try std.testing.expectEqualStrings(row.path, line[0 .. row.location_end - ":3:1".len]); starts[index] = row.code_start; } const content = file.content.ptr; const revision = file.revision; const index = try panes.File.lineIndex(p.gpa, file); result.body.cur_row = 8; result.body.cur_col = @intCast(file.location_rows[8].code_start); try panes.Output.fillResults(p, 0, "/", origin, "chunks", try p.gpa.dupe(u8, input.items), 8); try std.testing.expectEqual(result_id, source.search_pane.?); try std.testing.expectEqual(content, file.content.ptr); try std.testing.expectEqual(revision, file.revision); try std.testing.expectEqual(index.ptr, file.line_starts.ptr); try std.testing.expectEqual(@as(usize, 8), source.search_row.?); try std.testing.expectEqual(@as(i32, 8), result.body.cur_row); for (file.location_rows, starts) |row, start| try std.testing.expectEqual(start, row.code_start); // Simulate a restored view made by the previous global-padding // formatter. Its metadata still describes the same result bytes. const expected = try p.gpa.dupe(u8, file.content); defer p.gpa.free(expected); var global_width: usize = 0; for (widths) |width| global_width = @max(global_width, width); var legacy: std.ArrayList(u8) = .empty; defer legacy.deinit(p.gpa); for (file.location_rows, 0..) |*row, row_index| { const line = modal.lineSlice(file.content, row_index); const label = line[0..row.location_end]; const code = line[row.code_start..]; try legacy.appendSlice(p.gpa, label); const padding = global_width - panes.File.displayWidth(label); try legacy.appendNTimes(p.gpa, ' ', padding); try legacy.append(p.gpa, '\t'); try legacy.appendSlice(p.gpa, code); try legacy.append(p.gpa, '\n'); row.code_start = row.location_end + padding + 1; } const retained_rows = file.location_rows; file.location_rows = &.{}; panes.File.setContent(p, file, try legacy.toOwnedSlice(p.gpa)); file.location_rows = retained_rows; try std.testing.expect(!std.mem.eql(u8, expected, file.content)); try panes.Output.fillResults(p, 0, "/", origin, "chunks", try p.gpa.dupe(u8, input.items), 8); try std.testing.expectEqual(result_id, source.search_pane.?); try std.testing.expectEqualStrings(expected, file.content); for (file.location_rows, starts) |row, start| try std.testing.expectEqual(start, row.code_start); } } } test "result alignment chunks assign boundary context to nearest match with previous ties" { for ([_]panes.Output.Origin{ .search, .{ .query = .references } }) |origin| { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); defer p.deinit(); p.locations_config.layout = .@"inline"; var source_text: std.ArrayList(u8) = .empty; defer source_text.deinit(p.gpa); for (0..14) |_| try source_text.appendSlice(p.gpa, "\t source();\n"); const source = try p.setTestFile(source_text.items); p.gpa.free(source.file.?.path); source.file.?.path = try p.gpa.dupe(u8, "/chunk-boundary-界.txt"); p.locations_config = .{ .layout = .@"inline", .context = 2, .tscontext = false }; var input: std.ArrayList(u8) = .empty; defer input.deinit(p.gpa); for ([_]usize{ 1, 2, 3, 4, 5, 6, 7, 8, 12 }) |line| { const entry = try std.fmt.allocPrint(p.gpa, "{s}:{d}:1 \t source();\n", .{ source.file.?.path, line }); defer p.gpa.free(entry); try input.appendSlice(p.gpa, entry); } try panes.Output.fillResults(p, 0, "/", origin, "context chunks", try p.gpa.dupe(u8, input.items), 8); const file = &p.panes[source.search_pane.?].?.file.?; try std.testing.expectEqual(@as(usize, 14), file.location_rows.len); try std.testing.expectEqual(@as(usize, 11), source.search_row.?); const first_label = try std.fmt.allocPrint(p.gpa, "{s}:8:1", .{source.file.?.path}); defer p.gpa.free(first_label); const next_label = try std.fmt.allocPrint(p.gpa, "{s}:12:1", .{source.file.?.path}); defer p.gpa.free(next_label); for (file.location_rows, 0..) |row, index| { const line = modal.lineSlice(file.content, index); try std.testing.expectEqual(index + 1, row.at.line); const expected = if (row.at.line <= 10) first_label else next_label; try std.testing.expectEqual(panes.File.displayWidth(expected), panes.File.displayWidth(line[0 .. row.code_start - 1])); try std.testing.expectEqualStrings("\t source();", line[row.code_start..]); if (row.at.line >= 9 and row.at.line != 12) { try std.testing.expectEqual(.context, row.kind); try std.testing.expectEqual(@as(usize, 0), row.location_end); } else try std.testing.expectEqual(.match, row.kind); } } } test "declaration context ellipses mark only final header lines before omitted source" { if (!pardes.syntax.enabled) return error.SkipZigTest; const text = "const Outer = struct {\n" ++ "\n" ++ " pub fn run(\n" ++ " value: u32,\n" ++ " ) void {\n" ++ " const skipped = 1;\n" ++ " const Inner = struct {\n" ++ " pub fn inner(\n" ++ " input: u32,\n" ++ " ) void {\n" ++ " // neighboring source\n" ++ " _ = input;\n" ++ " // trailing neighbor\n" ++ " }\n" ++ " };\n" ++ " _ = value;\n" ++ " }\n" ++ "};\n"; for ([_]panes.Output.Origin{ .search, .{ .query = .references } }) |origin| { for ([_]bool{ false, true }) |visible| { for ([_]u8{ 0, 1 }) |neighbors| { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); defer p.deinit(); p.locations_config.layout = .@"inline"; const source = try p.setTestFile(text); p.gpa.free(source.file.?.path); source.file.?.path = try p.gpa.dupe(u8, "/ellipsis-context.zig"); p.locations_config = .{ .layout = .@"inline", .context = neighbors, .tscontext = true, .tslocations = visible }; const input = try std.fmt.allocPrint(p.gpa, "{s}:12:17 {s}\n{s}:16:9 {s}\n", .{ source.file.?.path, modal.lineSlice(text, 11), source.file.?.path, modal.lineSlice(text, 15), }); try panes.Output.fillResults(p, 0, "/", origin, "ellipsis", input, 1); const file = &p.panes[source.search_pane.?].?.file.?; var marked: usize = 0; var matches: usize = 0; var continuation_lines: usize = 0; for (file.location_rows, 0..) |row, index| { const shown = modal.lineSlice(file.content, index); const original = modal.lineSlice(text, row.at.line - 1); const expected_mark = row.at.line == 1 or row.at.line == 5 or (neighbors == 0 and row.at.line == 10); if (expected_mark) { try std.testing.expect(row.kind == .context and row.declaration); try std.testing.expectEqualStrings(original, shown[row.code_start .. shown.len - " ...".len]); try std.testing.expect(std.mem.endsWith(u8, shown, " ...")); marked += 1; } else try std.testing.expectEqualStrings(original, shown[row.code_start..]); if (row.at.line == 4 or row.at.line == 9) continuation_lines += 1; if (row.kind == .match) { matches += 1; try std.testing.expect(row.location_end > 0); if (row.at.line == 16) try std.testing.expectEqual(index, source.search_row.?); } else if (row.declaration) { const first_line = row.at.line == 1 or row.at.line == 3 or row.at.line == 7 or row.at.line == 8; try std.testing.expectEqual(visible and first_line, row.location_end > 0); try std.testing.expectEqualStrings(source.file.?.path, row.path); if (!first_line) try std.testing.expect(std.mem.indexOf(u8, shown[0..row.code_start], row.path) == null); } else try std.testing.expectEqual(@as(usize, 0), row.location_end); } try std.testing.expectEqual(@as(usize, if (neighbors == 0) 3 else 2), marked); try std.testing.expectEqual(@as(usize, 2), matches); try std.testing.expectEqual(@as(usize, 2), continuation_lines); try std.testing.expectEqualStrings(text, source.file.?.content); } } } } test "LSP occurrence tint follows the supplied source span through context" { const snippet = "\tfoo + 界 + foo;"; const text = "const Box = struct {\n fn run() void {\n" ++ snippet ++ "\n foo();\n }\n};\n"; for ([_]pardes.lsp.Kind{ .references, .definition }) |kind| { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 160, .rows = 40 }); defer p.deinit(); p.locations_config.layout = .@"inline"; const source = try p.setTestFile(text); p.gpa.free(source.file.?.path); source.file.?.path = try p.gpa.dupe(u8, "/occurrence.zig"); p.locations_config = .{ .layout = .@"inline", .context = 1, .tscontext = true }; p.presentation.enabled = false; var input: std.Io.Writer.Allocating = .init(p.gpa); defer input.deinit(); const start = std.mem.lastIndexOf(u8, snippet, "foo").?; try pardes.lsp.sourceSpanRow(&input.writer, source.file.?.path, 2, start, 2, start + 3, snippet); try panes.Output.fillResults(p, 0, "/", .{ .query = kind }, "", try input.toOwnedSlice(), 0); const id = source.search_pane.?; const file = &p.panes[id].?.file.?; var matches: usize = 0; for (file.location_rows, 0..) |row, index| { if (row.kind != .match) continue; const line = modal.lineSlice(file.content, index); try std.testing.expectEqualStrings(snippet, line[row.code_start..]); try std.testing.expectEqual(start + 1, row.at.col); try std.testing.expectEqual(start + 3, row.at.end_col); matches += 1; } try std.testing.expectEqual(@as(usize, 1), matches); pardes.layout.compute(p); var frame = std.heap.ArenaAllocator.init(p.gpa); defer frame.deinit(); const surface = try p.render(frame.allocator()); const chrome = pardes.ChromeTheme.fromTheme(p.theme()); var marked: usize = 0; for (surface.cells) |cell| { if (!std.meta.eql(cell.style.bg, pardes.Color{ .rgb = chrome.search_bg })) continue; try std.testing.expect(cell.text[0] == 'f' or cell.text[0] == 'o'); marked += 1; } try std.testing.expectEqual(@as(usize, 3), marked); try std.testing.expectEqualStrings(text, source.file.?.content); } } test "result context upper bound stops at earliest enclosing declaration" { if (!pardes.syntax.enabled) return error.SkipZigTest; const text = "// unrelated prelude\n" ++ "const unrelated = 1;\n" ++ "\n" ++ "const Outer = struct {\n" ++ " pub fn run(\n" ++ " value: u32,\n" ++ " ) void {\n" ++ " _ = value;\n" ++ " }\n" ++ "};\n" ++ "// trailing context remains available"; for ([_]panes.Output.Origin{ .search, .{ .query = .references } }) |origin| { for ([_]bool{ false, true }) |tscontext| { for ([_]bool{ false, true }) |visible| { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); defer p.deinit(); p.locations_config.layout = .@"inline"; const source = try p.setTestFile(text); p.gpa.free(source.file.?.path); source.file.?.path = try p.gpa.dupe(u8, "/bounded-context.zig"); p.locations_config = .{ .layout = .@"inline", .context = 100, .tscontext = tscontext, .tslocations = visible }; const input = try std.fmt.allocPrint(p.gpa, "{s}:8:13 {s}\n", .{ source.file.?.path, modal.lineSlice(text, 7) }); try panes.Output.fillResults(p, 0, "/", origin, "upper bound", input, 0); const file = &p.panes[source.search_pane.?].?.file.?; const first: usize = if (tscontext) 4 else 1; try std.testing.expectEqual(first, file.location_rows[0].at.line); try std.testing.expectEqual(@as(usize, 11), file.location_rows[file.location_rows.len - 1].at.line); try std.testing.expectEqual(@as(usize, 8) - first, source.search_row.?); var matches: usize = 0; for (file.location_rows, 0..) |row, index| { try std.testing.expectEqual(first + index, row.at.line); try std.testing.expectEqualStrings(modal.lineSlice(text, row.at.line - 1), modal.lineSlice(file.content, index)[row.code_start..]); if (row.kind == .match) { try std.testing.expectEqual(@as(usize, 8), row.at.line); matches += 1; } } try std.testing.expectEqual(@as(usize, 1), matches); try std.testing.expectEqualStrings(text, source.file.?.content); } } } // A top-level comment has no enclosing declaration, and a plain-text // file has no grammar. Neither should lose its preceding ordinary rows. for ([_][]const u8{ "/no-ancestor.zig", "/no-grammar.txt" }) |path| { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); defer p.deinit(); p.locations_config.layout = .@"inline"; const plain = "// first\n// second\n// matched\n// trailing"; const source = try p.setTestFile(plain); p.gpa.free(source.file.?.path); source.file.?.path = try p.gpa.dupe(u8, path); p.locations_config = .{ .layout = .@"inline", .context = 100, .tscontext = true, .tslocations = false }; const input = try std.fmt.allocPrint(p.gpa, "{s}:3:4 // matched\n", .{path}); try panes.Output.fillResults(p, 0, "/", .search, "unbounded", input, 0); const file = &p.panes[source.search_pane.?].?.file.?; try std.testing.expectEqual(@as(usize, 4), file.location_rows.len); try std.testing.expectEqual(@as(usize, 1), file.location_rows[0].at.line); try std.testing.expectEqual(@as(usize, 4), file.location_rows[3].at.line); try std.testing.expectEqual(@as(usize, 2), source.search_row.?); } } test "stacked search and LSP results retain source tint navigation and dump ownership" { if (!pardes.syntax.enabled) return error.SkipZigTest; const first = "\tfoo + 界 + foo;"; const second = " return foo;"; const source_text = "const Box = struct {\n fn run() void {\n" ++ first ++ "\n const neighbor = 42;\n" ++ second ++ "\n }\n};\n"; for ([_]panes.Output.Origin{ .search, .{ .query = .references } }) |origin| { for ([_]bool{ false, true }) |visible| { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 160, .rows = 50 }); defer p.deinit(); try std.testing.expectEqual(.stacked, p.locations_config.layout); p.locations_config = .{ .context = 1, .tscontext = true, .tslocations = visible }; p.presentation.enabled = false; const source = try p.setTestFile(source_text); p.gpa.free(source.file.?.path); source.file.?.path = try p.gpa.dupe(u8, "/stacked-界.zig"); var input: std.Io.Writer.Allocating = .init(p.gpa); defer input.deinit(); const first_col = std.mem.lastIndexOf(u8, first, "foo").?; const second_col = std.mem.lastIndexOf(u8, second, "foo").?; try pardes.lsp.sourceSpanRow(&input.writer, source.file.?.path, 2, first_col, 2, first_col + 3, first); try pardes.lsp.sourceSpanRow(&input.writer, source.file.?.path, 4, second_col, 4, second_col + 3, second); try panes.Output.fillResults(p, 0, "/", origin, "stacked", try p.gpa.dupe(u8, input.written()), 1); const id = source.search_pane.?; const pane = p.panes[id].?; const file = &pane.file.?; var matches: [2]usize = undefined; var count: usize = 0; var previews: usize = 0; var context_addresses: usize = 0; for (file.location_rows, 0..) |row, index| { const line = modal.lineSlice(file.content, index); switch (row.kind) { .match => { try std.testing.expectEqual(line.len, row.code_start); try std.testing.expect(row.location_end > 0); try std.testing.expect(!std.mem.containsAtLeast(u8, line, 1, "foo")); matches[count] = index; count += 1; }, .preview => { try std.testing.expect(index > 0); try std.testing.expectEqual(.match, file.location_rows[index - 1].kind); try std.testing.expectEqual(file.location_rows[index - 1].at, row.at); try std.testing.expectEqualStrings(file.location_rows[index - 1].path, row.path); try std.testing.expectEqual(@as(usize, 0), row.location_end); try std.testing.expectEqual(@as(usize, 0), row.code_start); try std.testing.expectEqualStrings(if (previews == 0) first else second, line); previews += 1; }, .context => { if (row.location_end > 0) { try std.testing.expect(visible and row.declaration); try std.testing.expectEqual(line.len, row.code_start); context_addresses += 1; } else { try std.testing.expectEqual(@as(usize, 0), row.code_start); try std.testing.expectEqualStrings(modal.lineSlice(source_text, row.at.line - 1), line); } }, } } try std.testing.expectEqual(@as(usize, 2), count); try std.testing.expectEqual(count, previews); try std.testing.expectEqual(@as(usize, if (visible) 2 else 0), context_addresses); try std.testing.expectEqual(matches[1], source.search_row.?); const original_content = try p.gpa.dupe(u8, file.content); defer p.gpa.free(original_content); try panes.Output.fillResults(p, 0, "/", origin, "stacked", try p.gpa.dupe(u8, input.written()), 0); try std.testing.expectEqual(id, source.search_pane.?); try std.testing.expectEqualStrings(original_content, file.content); pardes.layout.compute(p); var arena = std.heap.ArenaAllocator.init(p.gpa); defer arena.deinit(); const surface = try p.render(arena.allocator()); const chrome = pardes.ChromeTheme.fromTheme(p.theme()); var marked: usize = 0; for (surface.cells) |cell| { if (!std.meta.eql(cell.style.bg, pardes.Color{ .rgb = chrome.search_bg })) continue; try std.testing.expect(cell.text[0] == 'f' or cell.text[0] == 'o'); marked += 1; } try std.testing.expectEqual(@as(usize, 6), marked); const number = std.mem.indexOf(u8, file.content, "42").?; try std.testing.expectEqual(@intFromEnum(pardes.syntax.Syn.number), file.highlights[number]); p.active = id; pane.body.mode = .normal; pardes.look.armLookWalk(p, id); for ([_]struct { key: u21, match: usize }{ .{ .key = 'n', .match = 0 }, .{ .key = 'n', .match = 1 }, .{ .key = 'N', .match = 0 } }) |step| { p.update(.{ .key = .{ .cp = step.key } }); try std.testing.expectEqual(@as(i32, @intCast(matches[step.match])), pane.body.cur_row); const row = file.location_rows[matches[step.match]]; const line = modal.lineSlice(file.content, matches[step.match]); try std.testing.expectEqualStrings(line[0..row.location_end], selectedOutputText(pane) orelse ""); } try pardes.dump.dumpState(p); const restored = try pardes.dump.initFromDump(p.gpa, .{ .tty_only = true }, p.dump_out.?); defer restored.deinit(); const restored_file = &restored.panes[id].?.file.?; try std.testing.expectEqualStrings(file.content, restored_file.content); try std.testing.expectEqual(file.location_rows.len, restored_file.location_rows.len); for (file.location_rows, restored_file.location_rows) |before, after| { try std.testing.expectEqual(before.kind, after.kind); try std.testing.expectEqual(before.at, after.at); try std.testing.expectEqualStrings(before.path, after.path); try std.testing.expectEqual(before.code_start, after.code_start); try std.testing.expectEqual(before.location_end, after.location_end); try std.testing.expectEqualSlices(u8, before.colors, after.colors); } restored.active = id; pardes.look.armLookWalk(restored, id); // The dot came back on the match it was on; the walk goes on // from it both ways, N wrapping to the last as it does live. try std.testing.expectEqual(@as(i32, @intCast(matches[0])), restored.panes[id].?.body.cur_row); restored.update(.{ .key = .{ .cp = 'N' } }); try std.testing.expectEqual(@as(i32, @intCast(matches[1])), restored.panes[id].?.body.cur_row); restored.update(.{ .key = .{ .cp = 'n' } }); try std.testing.expectEqual(@as(i32, @intCast(matches[0])), restored.panes[id].?.body.cur_row); try std.testing.expectEqualStrings(source_text, source.file.?.content); } } } test "location analysis cache refreshes unsaved ancestry and query context" { if (!pardes.syntax.enabled) return error.SkipZigTest; const before = "const Outer = struct {\n" ++ " fn first() void {\n" ++ " marker();\n" ++ " }\n" ++ " fn second() void {\n" ++ " marker();\n" ++ " }\n" ++ "};\n"; const after = "pub fn revised() void {\n // new neighbor\n marker();\n}\n"; const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); defer p.deinit(); const source = try p.setTestFile(before); p.gpa.free(source.file.?.path); source.file.?.path = try p.gpa.dupe(u8, "/analysis-cache.zig"); p.locations_config = .{ .context = 0, .tscontext = true, .tslocations = false }; const Runner = struct { fn fill(editor: *Pardes, line: usize) !void { const path = editor.panes[0].?.file.?.path; const input = try std.fmt.allocPrint(editor.gpa, "{s}:{d}:9-14 marker();\n", .{ path, line }); try panes.Output.fillResults(editor, 0, "/", .search, "marker", input, 0); } }; try Runner.fill(p, 3); const id = source.search_pane.?; const file = &p.panes[id].?.file.?; try std.testing.expect(std.mem.indexOf(u8, file.content, "const Outer") != null); try std.testing.expect(std.mem.indexOf(u8, file.content, "fn first") != null); try std.testing.expect(std.mem.indexOf(u8, file.content, "fn second") == null); const original = file.content.ptr; const revision = file.revision; const analyses = p.locations_cache.analyses; const hits = p.locations_cache.hits; try Runner.fill(p, 3); try std.testing.expectEqual(id, source.search_pane.?); try std.testing.expectEqual(original, file.content.ptr); try std.testing.expectEqual(revision, file.revision); try std.testing.expectEqual(analyses, p.locations_cache.analyses); try std.testing.expect(p.locations_cache.hits > hits); // Reusing analysis must still choose the ancestry of the current match. try Runner.fill(p, 6); try std.testing.expectEqual(analyses, p.locations_cache.analyses); try std.testing.expect(std.mem.indexOf(u8, file.content, "const Outer") != null); try std.testing.expect(std.mem.indexOf(u8, file.content, "fn first") == null); try std.testing.expect(std.mem.indexOf(u8, file.content, "fn second") != null); const match = file.location_rows[source.search_row.?]; try std.testing.expectEqual(.match, match.kind); try std.testing.expectEqual(@as(usize, 6), match.at.line); // The path stays identical, but an unsaved replacement changes its tree. panes.File.setContent(p, &source.file.?, try p.gpa.dupe(u8, after)); try Runner.fill(p, 3); try std.testing.expect(p.locations_cache.analyses > analyses); try std.testing.expect(std.mem.indexOf(u8, file.content, "fn revised") != null); try std.testing.expect(std.mem.indexOf(u8, file.content, "const Outer") == null); try std.testing.expect(std.mem.indexOf(u8, file.content, "fn second") == null); try std.testing.expect(std.mem.indexOf(u8, file.content, "new neighbor") == null); p.locations_config.context = 1; try Runner.fill(p, 3); try std.testing.expect(std.mem.indexOf(u8, file.content, "fn revised") != null); try std.testing.expect(std.mem.indexOf(u8, file.content, "new neighbor") != null); const expanded = try p.gpa.dupe(u8, file.content); defer p.gpa.free(expanded); const expanded_analyses = p.locations_cache.analyses; p.locations_config.context = 0; try Runner.fill(p, 3); try std.testing.expect(std.mem.indexOf(u8, file.content, "new neighbor") == null); p.locations_config.context = 1; try Runner.fill(p, 3); try std.testing.expectEqualStrings(expanded, file.content); try std.testing.expectEqual(expanded_analyses, p.locations_cache.analyses); p.locations_config.tscontext = false; try Runner.fill(p, 3); try std.testing.expect(std.mem.indexOf(u8, file.content, "fn revised") == null); try std.testing.expect(std.mem.indexOf(u8, file.content, "new neighbor") != null); try std.testing.expectEqualStrings(after, source.file.?.content); } test "location cache eviction retains early colors and honors disk and unsaved replacements" { if (!pardes.syntax.enabled) return error.SkipZigTest; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var dir_buf: [4096]u8 = undefined; const dir = dir_buf[0..try tmp.dir.realPath(std.testing.io, &dir_buf)]; const comment = "/* open\nconst int value = 42;\n*/\nneedle();\n"; const code = "// open\nconst int value = 42;\n//\nneedle();\n"; try std.testing.expectEqual(comment.len, code.len); const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); defer p.deinit(); const source = try p.setTestFile("notes\n"); p.locations_config = .{ .context = 3, .tscontext = false }; var input: std.Io.Writer.Allocating = .init(p.gpa); defer input.deinit(); for (0..65) |index| { var name_buf: [32]u8 = undefined; const name = try std.fmt.bufPrint(&name_buf, "f{d:0>2}.c", .{index}); try tmp.dir.writeFile(std.testing.io, .{ .sub_path = name, .data = if (index == 0) comment else code }); try input.writer.print("{s}:4:1-6 needle();\n", .{name}); } try panes.Output.fillResults(p, 0, dir, .search, "eviction", try p.gpa.dupe(u8, input.written()), null); const id = source.search_pane.?; const file = &p.panes[id].?.file.?; try std.testing.expectEqual(@as(usize, 64), p.locations_cache.len); try std.testing.expectEqual(@as(usize, 65), p.locations_cache.analyses); const Color = struct { fn valueColor(result: *const panes.File.State) !u8 { for (result.location_rows, 0..) |row, index| { if (row.kind != .context or row.at.line != 2 or !std.mem.endsWith(u8, row.path, "f00.c")) continue; const line = modal.lineSlice(result.content, index); const number = std.mem.indexOf(u8, line[row.code_start..], "42").?; try std.testing.expectEqualStrings("const int value = 42;", line[row.code_start..]); try std.testing.expectEqual(line.len - row.code_start, row.colors.len); return row.colors[number]; } return error.MissingEarlyContext; } }; try std.testing.expectEqual(@intFromEnum(pardes.syntax.Syn.comment), try Color.valueColor(file)); const single = "f00.c:4:1-6 needle();\n"; // Put the evicted file back before a same-size edit tests exact-byte keys. try panes.Output.fillResults(p, 0, dir, .search, "eviction", try p.gpa.dupe(u8, single), null); const analyses = p.locations_cache.analyses; try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "f00.c", .data = code }); try panes.Output.fillResults(p, 0, dir, .search, "eviction", try p.gpa.dupe(u8, single), null); try std.testing.expect(p.locations_cache.analyses > analyses); try std.testing.expectEqual(@intFromEnum(pardes.syntax.Syn.number), try Color.valueColor(file)); // An open buffer has priority over that disk revision, even with equal size. p.gpa.free(source.file.?.path); source.file.?.path = try std.fs.path.join(p.gpa, &.{ dir, "f00.c" }); panes.File.setContent(p, &source.file.?, try p.gpa.dupe(u8, comment)); try panes.Output.fillResults(p, 0, dir, .search, "eviction", try p.gpa.dupe(u8, single), null); try std.testing.expectEqual(@intFromEnum(pardes.syntax.Syn.comment), try Color.valueColor(file)); try std.testing.expectEqualStrings(comment, source.file.?.content); }