summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-15 16:44:18 -0300
committerGabriel Schneider <[email protected]>2026-09-15 17:24:42 -0300
commit96725011b43d7bf56ac02e710ea4ef4a76f05351 (patch)
tree3fb1dd233337a6023fe11a8e1ad777d6f44accec
parent89b3c352378eb0284b521722a13c4245cab41b0c (diff)
downloadpardes-96725011b43d7bf56ac02e710ea4ef4a76f05351.tar.gz
pardes-96725011b43d7bf56ac02e710ea4ef4a76f05351.zip
Make location highlighting an explicit output producer choice
-rw-r--r--docs/config.md4
-rw-r--r--src/builtins.zig8
-rw-r--r--src/panes.zig30
-rw-r--r--src/pardes.zig19
-rw-r--r--test/output.zig79
5 files changed, 122 insertions, 18 deletions
diff --git a/docs/config.md b/docs/config.md
index 2a1825ce..42163f2b 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -138,6 +138,10 @@ it. `Config` reports the appearance option, and dumps preserve it.
Search, Grep and LSP location-result panes include `LocationsConfig` in their
default tags. Custom tags keep their edits.
+Location highlighting is enabled by the command that produces a location list.
+Plain reports, including `LocationsConfig`, keep ordinary text colors even
+when their text resembles a location. `Mini` keeps its own syntax colors.
+
`LocationsConfig` prints the current settings for Search, Grep and LSP location
results in an output pane. Execute the printed line to apply it again, or
supply just the fields to change:
diff --git a/src/builtins.zig b/src/builtins.zig
index 18ea0198..765fa93c 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -95,6 +95,8 @@ pub const OutputTraits = struct {
name: []const u8,
steps: bool = false,
jumps: bool = false,
+ // This producer emits location rows whose labels and source excerpts may be styled.
+ locations: bool = false,
commands: bool = false,
doc: bool = false,
// Saving promotes this scratch buffer into an ordinary file.
@@ -357,7 +359,7 @@ pub const PdfTint = struct {
};
pub const PdfSections = struct {
- pub const output: OutputTraits = .{ .name = config.pdf_sections_buffer, .steps = true };
+ pub const output: OutputTraits = .{ .name = config.pdf_sections_buffer, .steps = true, .locations = true };
pub const enabled = pardes.pdf_enabled;
pub fn run(c: Ctx) void {
if (comptime enabled) apply(c) else unreachable;
@@ -618,7 +620,7 @@ pub const Find = struct {
/// matches file CONTENTS under every pane's directory at once.
pub const Grep = struct {
pub const takes_arg = true;
- pub const output: OutputTraits = .{ .name = config.search_buffer, .steps = true };
+ pub const output: OutputTraits = .{ .name = config.search_buffer, .steps = true, .locations = true };
pub fn run(c: Ctx) void {
const pat = std.mem.trim(u8, c.arg orelse "", " \t\r\n");
if (pat.len > 0) return c.p.runSearch(c.id, pat, .grep, .top) catch |err|
@@ -682,7 +684,7 @@ pub const Last = struct {
};
pub const Jumplist = struct {
- pub const output: OutputTraits = .{ .name = config.jumps_buffer, .steps = true };
+ pub const output: OutputTraits = .{ .name = config.jumps_buffer, .steps = true, .locations = true };
pub fn run(c: Ctx) void {
panes.Output.openJumps(c.p, c.id) catch |err| c.p.reportError(c.id, "jumplist", err);
}
diff --git a/src/panes.zig b/src/panes.zig
index 886d4b28..315b0b0f 100644
--- a/src/panes.zig
+++ b/src/panes.zig
@@ -271,8 +271,13 @@ pub const Pane = struct {
pub fn colorAlgo(pane: *const Pane) ColorAlgo {
if (pane.isTerminal()) return .tty;
if (pane.file) |f| {
+ if (f.mini != null) return .source;
if (std.mem.endsWith(u8, f.path, ".diff") or std.mem.endsWith(u8, f.path, ".patch")) return .diff;
- if (f.output != null and !Output.fileTraits(f.output).saves) return .locations;
+ if (f.output != null) {
+ const tr = Output.fileTraits(f.output);
+ if (tr.locations) return .locations;
+ if (!tr.saves) return .none;
+ }
return .source;
}
return .none;
@@ -1811,6 +1816,7 @@ pub const File = struct {
else
@max(start, lineStart(p.gpa, f, f.scroll + pane.rows + SYNTAX_CONTEXT_AFTER_ROWS + slack));
const new_highlights = (switch (pane.colorAlgo()) {
+ .none => @as([]u8, &.{}),
.diff => syntax.highlightDiff(p.tree_sitter_gpa, f.content, start, end),
.locations => if (f.location_rows.len > 0)
syntax.highlightLocationRows(p.tree_sitter_gpa, f.content, f.location_rows)
@@ -2187,6 +2193,7 @@ pub const Output = struct {
fn decorateRow(output: ?State, line: []const u8, metadata: ?locations.Row) RowDecoration {
const origin = (output orelse return .{}).from;
+ if (!traits(origin).locations) return .{};
const target = locations.parse(line);
const spot = if (metadata) |m| m.at else target.at;
if (spot.line == 0) return .{};
@@ -2238,6 +2245,18 @@ pub const Output = struct {
try std.testing.expect(malformed.mark_end <= "src/a.rs:3:99999999999999999999-99999999999999999999 text".len);
}
+ test "plain output does not opt into location decoration" {
+ const report = rowDecoration(.{ .from = .{ .cmd = .LocationsConfig } }, "LocationsConfig context:5 tscontext:on");
+ try std.testing.expectEqualDeep(RowDecoration{}, report);
+ const prose = rowDecoration(.{ .from = .{ .query = .hover } }, "src/a.rs:3:5-7 error: prose");
+ try std.testing.expectEqualDeep(RowDecoration{}, prose);
+ const errors = rowDecoration(.{ .from = .errors }, "src/a.rs:3:5-7 error: prose");
+ try std.testing.expectEqualDeep(RowDecoration{}, errors);
+ try std.testing.expect(!traits(.{ .cmd = .Find }).locations);
+ try std.testing.expect(traits(.{ .cmd = .Grep }).locations);
+ try std.testing.expect(traits(.{ .query = .completion }).locations);
+ }
+
pub fn setArg(o: *State, text: []const u8) error{ArgumentTooLong}!void {
if (text.len > max_arg) return error.ArgumentTooLong;
o.arg_len = @intCast(text.len);
@@ -2251,18 +2270,19 @@ pub const Output = struct {
pub fn traits(o: Origin) Traits {
return switch (o) {
// rows are `location text`, so n/N walk them
- .search => .{ .name = config.search_buffer, .steps = true },
+ .search => .{ .name = config.search_buffer, .steps = true, .locations = true },
.errors => .{ .name = config.errors_buffer, .doc = true },
- .pdf_links => .{ .name = config.pdf_links_buffer, .steps = true },
+ .pdf_links => .{ .name = config.pdf_links_buffer, .steps = true, .locations = true },
.cmd => |b| builtins.registry.outputTraits(b) orelse unreachable,
.query => |k| switch (k) {
.hover => .{ .name = config.hover_buffer },
// prose: an action list, a diff, a report about the backend
.code_action, .format, .status, .explain => .{ .name = config.lsp_buffer },
- .rename => .{ .name = config.search_buffer, .steps = true },
+ .rename => .{ .name = config.search_buffer, .steps = true, .locations = true },
.definition, .declaration, .type_definition, .implementation, .references => .{
.name = config.search_buffer,
.steps = true,
+ .locations = true,
.jumps = true,
},
// The hierarchy kinds behave like references: a list of places,
@@ -2270,11 +2290,13 @@ pub const Output = struct {
.incoming_calls, .outgoing_calls, .supertypes, .subtypes => .{
.name = config.search_buffer,
.steps = true,
+ .locations = true,
.jumps = true,
},
.document_symbols, .workspace_symbols, .diagnostics, .workspace_diagnostics, .select_refs, .completion => .{
.name = config.search_buffer,
.steps = true,
+ .locations = true,
},
},
};
diff --git a/src/pardes.zig b/src/pardes.zig
index 56f95040..25f4e18e 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -13827,17 +13827,14 @@ pub const Pardes = struct {
}
const tz_color = tracy.zone(@src(), "paneRecolor");
- switch (pane.colorAlgo()) {
- .tty => if (p.settings.colors) panes.Terminal.recolorAnsi(p, pane, r, tx, tw, body_h, body),
- // `.locations` joins them because it feeds the same `f.highlights`
- // — only the pass that FILLED it differs (refreshHighlights).
- .source, .diff, .locations => {
- const f = &pane.file.?;
- panes.File.drawGutter(p, pane, r, tx, tw, body_h, active);
- if (p.settings.colors) panes.File.recolorSyntax(p, pane, f, r, tx, tw, body_h);
- panes.File.drawWrapMarkers(p, pane, r, tx, tw, body_h, pane_bg);
- },
- .none => {},
+ // File layout is independent of whether its producer requests colors.
+ if (pane.file) |*f| {
+ panes.File.drawGutter(p, pane, r, tx, tw, body_h, active);
+ if (p.settings.colors and pane.colorAlgo() != .none)
+ panes.File.recolorSyntax(p, pane, f, r, tx, tw, body_h);
+ panes.File.drawWrapMarkers(p, pane, r, tx, tw, body_h, pane_bg);
+ } else if (pane.isTerminal() and p.settings.colors) {
+ panes.Terminal.recolorAnsi(p, pane, r, tx, tw, body_h, body);
}
tz_color.end();
diff --git a/test/output.zig b/test/output.zig
index 2ccd951a..f1d9d4e0 100644
--- a/test/output.zig
+++ b/test/output.zig
@@ -72,6 +72,85 @@ test "search tint survives wrapping and theme changes while selections remain au
}
}
+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 = if (p.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H;
+ 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();
+ _ = 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();
+ _ = 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";