diff --git a/src/tui.zig b/src/tui.zig index c331dd6..33265b2 100644 --- a/src/tui.zig +++ b/src/tui.zig @@ -430,7 +430,7 @@ pub const Client = struct { } for (list_rows) |row| { if (body_lines_used >= max_body_lines) break; - try appendThemedRow(allocator, &out, row, self.viewport.width, ansi_text); + try appendPanelRow(allocator, &out, row, self.viewport.width); try out.append(allocator, '\n'); body_lines_used += 1; } @@ -2586,6 +2586,13 @@ const ansi_syntax_builtin = "\x1b[38;2;121;155;224;48;2;25;22;33m"; const ansi_syntax_string = "\x1b[38;2;116;196;171;48;2;25;22;33m"; const ansi_syntax_comment = "\x1b[38;2;103;93;122;48;2;25;22;33m"; const ansi_syntax_number = "\x1b[38;2;231;168;78;48;2;25;22;33m"; +const ansi_diag_error = "\x1b[38;2;238;84;88;48;2;25;22;33m"; +const ansi_diag_warning = "\x1b[38;2;231;168;78;48;2;25;22;33m"; +const ansi_diag_info = "\x1b[38;2;121;155;224;48;2;25;22;33m"; +const ansi_diag_hint = "\x1b[38;2;116;196;171;48;2;25;22;33m"; +const ansi_success = "\x1b[38;2;116;196;171;48;2;25;22;33m"; +const ansi_stale = "\x1b[38;2;103;93;122;48;2;25;22;33m"; +const ansi_provider = "\x1b[38;2;235;119;84;48;2;25;22;33m"; const ansi_status = "\x1b[38;2;25;22;33;48;2;235;119;84m"; fn appendEditorLine( @@ -2768,6 +2775,28 @@ fn appendThemedRow(allocator: std.mem.Allocator, out: *std.ArrayList(u8), text: try out.appendSlice(allocator, ansi_reset); } +fn appendPanelRow(allocator: std.mem.Allocator, out: *std.ArrayList(u8), row: []const u8, width: usize) !void { + try appendThemedRow(allocator, out, row, width, panelRowStyle(row)); +} + +fn panelRowStyle(row: []const u8) []const u8 { + if (startsAny(row, &.{ "job:status:exit_0", "terminal:status:exit_0", "ok:", "success:" })) return ansi_success; + if (startsAny(row, &.{ "error:", "stderr:", "diagnostic:error", "job:status:exit_", "job:status:spawn_error_", "terminal:status:exit_" })) return ansi_diag_error; + if (startsAny(row, &.{ "warning:", "warn:", "diagnostic:warning", "timeout:", "job:status:timeout", "terminal:status:timeout" })) return ansi_diag_warning; + if (startsAny(row, &.{ "info:", "diagnostic:info", "stdout:", "job:status:running", "terminal:status:running" })) return ansi_diag_info; + if (startsAny(row, &.{ "hint:", "diagnostic:hint" })) return ansi_diag_hint; + if (startsAny(row, &.{ "provider:", "lsp-provider:", "language-provider:" })) return ansi_provider; + if (startsAny(row, &.{ "missing:", "conflict:", "stale:", "job_status:idle", "terminal_status:idle" })) return ansi_stale; + return ansi_text; +} + +fn startsAny(row: []const u8, prefixes: []const []const u8) bool { + for (prefixes) |prefix| { + if (std.mem.startsWith(u8, row, prefix)) return true; + } + return false; +} + fn appendStatusLine(allocator: std.mem.Allocator, out: *std.ArrayList(u8), status: []const u8, width: usize) !void { const line_start = out.items.len; try out.appendSlice(allocator, ansi_status); @@ -5550,3 +5579,33 @@ test "regular: plain text without syntax spans stays normal themed text" { try std.testing.expect(std.mem.indexOf(u8, frame, ansi_text) != null); try std.testing.expect(std.mem.indexOf(u8, frame, ansi_syntax_keyword) == null); } + +test "regular: panel rows style diagnostic, job, provider, and stale states" { + try std.testing.expectEqualStrings(ansi_diag_error, panelRowStyle("error:2:5:file:///repo/src/main.zig:expected_semicolon")); + try std.testing.expectEqualStrings(ansi_diag_warning, panelRowStyle("warning:3:1:file:///repo/src/main.zig:unused_var")); + try std.testing.expectEqualStrings(ansi_diag_info, panelRowStyle("info:1:1:file:///repo/src/main.zig:note")); + try std.testing.expectEqualStrings(ansi_diag_hint, panelRowStyle("hint:1:1:file:///repo/src/main.zig:tip")); + try std.testing.expectEqualStrings(ansi_diag_error, panelRowStyle("job:status:exit_1")); + try std.testing.expectEqualStrings(ansi_success, panelRowStyle("job:status:exit_0")); + try std.testing.expectEqualStrings(ansi_diag_error, panelRowStyle("job:status:spawn_error_FileNotFound")); + try std.testing.expectEqualStrings(ansi_diag_warning, panelRowStyle("timeout:recoverable")); + try std.testing.expectEqualStrings(ansi_provider, panelRowStyle("provider:zig:zls:available")); + try std.testing.expectEqualStrings(ansi_stale, panelRowStyle("missing:provider:zls")); + try std.testing.expectEqualStrings(ansi_text, panelRowStyle("plain row")); +} + +test "regular: appended panel rows carry semantic color identity" { + var out: std.ArrayList(u8) = .empty; + defer out.deinit(std.testing.allocator); + + try appendPanelRow(std.testing.allocator, &out, "error:2:5:file:///repo/src/main.zig:expected_semicolon", 72); + try std.testing.expect(std.mem.indexOf(u8, out.items, ansi_diag_error) != null); + out.clearRetainingCapacity(); + + try appendPanelRow(std.testing.allocator, &out, "job:status:exit_0", 72); + try std.testing.expect(std.mem.indexOf(u8, out.items, ansi_success) != null); + out.clearRetainingCapacity(); + + try appendPanelRow(std.testing.allocator, &out, "provider:zig:zls:available", 72); + try std.testing.expect(std.mem.indexOf(u8, out.items, ansi_provider) != null); +} diff --git a/tools/terminal_e2e.py b/tools/terminal_e2e.py index b9150cc..7c3c772 100755 --- a/tools/terminal_e2e.py +++ b/tools/terminal_e2e.py @@ -101,6 +101,12 @@ def classify_sgr(params: str) -> str: return "syntax-comment" if "38;2;231;168;78" in joined: return "syntax-number" + if "38;2;238;84;88" in joined: + return "diag-error" + if "38;2;116;196;171" in joined: + return "diag-hint" + if "38;2;121;155;224" in joined: + return "diag-info" if "48;2;20;18;26" in joined: return "current-line" if "48;2;235;119;84" in joined: @@ -648,6 +654,9 @@ def run_one(mim: Path, root_out: Path, scenario: Scenario) -> tuple[str, Path]: ("38;2;116;196;171", "syntax-string"), ("38;2;103;93;122", "syntax-comment"), ("38;2;231;168;78", "syntax-number"), + ("38;2;238;84;88", "diag-error"), + ("38;2;116;196;171", "diag-hint"), + ("38;2;121;155;224", "diag-info"), ): if sgr in raw_text: raw_attrs.add(attr)