summaryrefslogtreecommitdiff
path: root/src/panes.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/panes.zig')
-rw-r--r--src/panes.zig766
1 files changed, 1 insertions, 765 deletions
diff --git a/src/panes.zig b/src/panes.zig
index bc579a92..6ee7d70b 100644
--- a/src/panes.zig
+++ b/src/panes.zig
@@ -2408,771 +2408,7 @@ pub const File = struct {
}
};
-pub const Output = struct {
- const Builtin = builtins.registry.Builtin();
- const gui_shader_source_mode = effect_sources.guiShaderSourceMode();
-
- const fonts = if (builtins.capabilities.font_picker) @import("fonts.zig") else struct {};
-
- pub const Origin = union(enum) {
- cmd: Builtin,
- query: lsp.Kind,
- search,
- errors,
- pdf_links,
- };
-
- pub const max_arg = dump.max_origin_arg;
-
- pub const State = struct {
- from: Origin,
- arg_buf: [max_arg]u8 = undefined,
- arg_len: u16 = 0,
-
- pub fn arg(o: *const State) []const u8 {
- return o.arg_buf[0..o.arg_len];
- }
- };
-
- const RowDecoration = struct {
- location_end: usize = 0,
- prefix_end: usize = 0,
- declaration: bool = false,
- mark_start: usize = 0,
- mark_end: usize = 0,
- severity: enum { none, err, warning, info, hint } = .none,
-
- fn styleAt(row: RowDecoration, p: *const Pardes, byte: usize) ?pardes.CellStyle {
- // Body paper changes immediately, so its ink must change with it
- // even while the surrounding tags are still fading.
- const chrome = pardes.ChromeTheme.fromTheme(p.theme());
- if (row.declaration or byte < @max(row.location_end, row.prefix_end))
- return .{ .fg = .{ .rgb = chrome.lineno } };
- if (byte < row.mark_start or byte >= row.mark_end) return null;
- if (row.severity == .none) return .{
- .fg = .{ .rgb = chrome.search_fg },
- .bg = .{ .rgb = chrome.search_bg },
- };
- return .{ .fg = .{ .rgb = switch (row.severity) {
- .err => chrome.diagnostic_error,
- .warning => chrome.diagnostic_warning,
- .info => chrome.diagnostic_info,
- .hint => chrome.diagnostic_hint,
- .none => unreachable,
- } }, .bold = true };
- }
- };
-
- fn rowDecoration(output: ?State, line: []const u8) RowDecoration {
- return decorateRow(output, line, null);
- }
-
- 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 .{};
- var row: RowDecoration = .{
- .location_end = if (metadata) |m| m.location_end else target.end,
- .prefix_end = if (metadata) |m| m.code_start else target.end,
- .declaration = if (metadata) |m| m.declaration else false,
- };
- const source_start = if (metadata) |m| m.code_start else target.code_start;
- if (source_start >= line.len) return row;
- if (metadata) |m| if (m.kind == .context) return row;
- const mark_occurrence = switch (origin) {
- .search => true,
- .cmd => |cmd| cmd == .Grep,
- .query => |kind| switch (kind) {
- .definition, .declaration, .type_definition, .implementation, .references => true,
- else => false,
- },
- else => false,
- };
- if (mark_occurrence and spot.col > 0 and spot.end_line == spot.line and
- spot.end_col >= spot.col)
- {
- row.mark_start = source_start +| (spot.col - 1);
- row.mark_end = @min(line.len, source_start +| spot.end_col);
- }
- const is_diagnostic = switch (origin) {
- .query => |kind| kind == .diagnostics or kind == .workspace_diagnostics,
- else => false,
- };
- if (is_diagnostic) {
- const text = line[source_start..];
- inline for (.{ .{ "error:", .err }, .{ "warning:", .warning }, .{ "info:", .info }, .{ "hint:", .hint } }) |label| {
- if (std.mem.startsWith(u8, text, label[0])) {
- row.mark_start = source_start;
- row.mark_end = source_start + label[0].len;
- row.severity = label[1];
- }
- }
- }
- return row;
- }
-
- test "result decoration follows source byte ranges and diagnostic provenance" {
- const row = rowDecoration(.{ .from = .search }, "src/a.rs:3:5-7 let abc = 1;");
- try std.testing.expectEqualStrings("abc", "src/a.rs:3:5-7 let abc = 1;"[row.mark_start..row.mark_end]);
- const prose = rowDecoration(.{ .from = .{ .query = .hover } }, "src/a.rs:3:5-7 error: prose");
- try std.testing.expectEqual(@as(usize, 0), prose.mark_end);
- const diagnostic = rowDecoration(.{ .from = .{ .query = .diagnostics } }, "src/a.rs:3:5-7 warning: unused");
- try std.testing.expectEqual(.warning, diagnostic.severity);
- try std.testing.expectEqualStrings("warning:", "src/a.rs:3:5-7 warning: unused"[diagnostic.mark_start..diagnostic.mark_end]);
- const malformed = rowDecoration(.{ .from = .search }, "src/a.rs:3:99999999999999999999-99999999999999999999 text");
- try std.testing.expect(malformed.mark_end <= "src/a.rs:3:99999999999999999999-99999999999999999999 text".len);
- const snippet = "src/a.rs:3:14-16 \t\tfoo + 界 + foo";
- for ([_]lsp.Kind{ .definition, .declaration, .type_definition, .implementation, .references }) |kind| {
- const occurrence = rowDecoration(.{ .from = .{ .query = kind } }, snippet);
- try std.testing.expectEqualStrings("foo", snippet[occurrence.mark_start..occurrence.mark_end]);
- }
- for ([_]lsp.Kind{ .document_symbols, .workspace_symbols, .completion, .incoming_calls, .outgoing_calls, .supertypes, .subtypes, .rename }) |kind| {
- const label = rowDecoration(.{ .from = .{ .query = kind } }, snippet);
- try std.testing.expectEqual(@as(usize, 0), label.mark_end);
- }
- }
-
- test "stacked empty previews distinguish separators from trailing newlines" {
- var path = [_]u8{ 'x', '.', 'c' };
- const rows = [_]locations.Row{
- .{ .kind = .match, .path = &path, .at = .{ .line = 1, .col = 1 }, .location_end = 7, .code_start = 7 },
- .{ .kind = .preview, .path = &path, .at = .{ .line = 1, .col = 1 }, .location_end = 0, .code_start = 0 },
- };
- try std.testing.expect(locations.sameInput("x.c:1:1 ", "x.c:1:1\n", &rows, .stacked));
- try std.testing.expect(locations.sameInput("x.c:1:1 \n", "x.c:1:1\n\n", &rows, .stacked));
- try std.testing.expect(!locations.sameInput("x.c:1:1 ", "x.c:1:1\n\n", &rows, .stacked));
- try std.testing.expect(!locations.sameInput("x.c:1:1 \n", "x.c:1:1\n", &rows, .stacked));
- }
-
- 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);
- @memcpy(o.arg_buf[0..o.arg_len], text);
- }
-
- pub const Traits = builtins.OutputTraits;
-
- const file_row: Traits = .{ .name = "", .doc = true, .saves = true };
-
- 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, .locations = true },
- .errors => .{ .name = config.errors_buffer, .doc = 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, .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,
- // and a lone answer (one caller, one subtype) is a jump.
- .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,
- },
- },
- };
- }
-
- pub fn usesLocationsConfig(from: Origin) bool {
- const tr = traits(from);
- return tr.steps and !tr.commands and from != .pdf_links and
- !std.meta.eql(from, Origin{ .cmd = .Find }) and
- !std.meta.eql(from, Origin{ .query = .completion });
- }
-
- pub fn fileTraits(out: ?State) Traits {
- return traits((out orelse return file_row).from);
- }
-
- pub const Grain = enum {
- word,
- line,
- whole,
- };
-
- pub fn grain(out: ?State) Grain {
- const tr = fileTraits(out);
- if (tr.commands) return .whole;
- return if (tr.steps) .line else .word;
- }
-
- pub fn word(o: Origin) []const u8 {
- return switch (o) {
- .cmd => |b| @tagName(b),
- .query => |k| @tagName(k),
- .search => "/",
- .errors => config.errors_buffer,
- .pdf_links => config.pdf_links_buffer,
- };
- }
-
- pub fn fromWord(w: []const u8) ?Origin {
- if (w.len == 0) return null;
- if (std.mem.eql(u8, w, "/")) return .search;
- if (std.mem.eql(u8, w, config.errors_buffer)) return .errors;
- if (std.mem.eql(u8, w, config.pdf_links_buffer)) return .pdf_links;
- if (std.meta.stringToEnum(Builtin, w)) |b|
- if (builtins.registry.outputTraits(b) != null) return .{ .cmd = b };
- if (std.meta.stringToEnum(lsp.Kind, w)) |k| return .{ .query = k };
- return null;
- }
-
- pub fn resultsFrom(p: *Pardes, pane: *Pane, from: Origin) bool {
- const rp = p.panes[pane.search_pane orelse return false] orelse return false;
- const f = rp.file orelse return false;
- const o = f.output orelse return false;
- return std.meta.eql(o.from, from);
- }
-
- pub const Location = struct {
- path: []const u8,
- at: look.Spot,
- end: usize,
-
- fn order(a: Location, b: Location) std.math.Order {
- const path = std.mem.order(u8, a.path, b.path);
- if (path != .eq) return path;
- if (a.at.line != b.at.line) return std.math.order(a.at.line, b.at.line);
- return std.math.order(a.at.col, b.at.col);
- }
- };
-
- pub fn location(line: []const u8) Location {
- const target = locations.parse(std.mem.trimEnd(u8, line, " \t\r\n"));
- return .{ .path = target.path, .at = target.at, .end = target.end };
- }
-
- pub fn isResultRow(file: *const File.State, row: usize) bool {
- if (row < file.location_rows.len) return file.location_rows[row].kind == .match;
- return !locations.isContextLine(modal.lineSlice(file.content, row));
- }
-
- fn sortResults(arena: std.mem.Allocator, from: Origin, content: []u8, anchor: ?usize) !?usize {
- switch (from) {
- .search => {},
- .cmd => |cmd| switch (cmd) {
- .Find, .Grep => {},
- else => return anchor,
- },
- .query => |kind| switch (kind) {
- .definition,
- .declaration,
- .type_definition,
- .implementation,
- .references,
- .incoming_calls,
- .outgoing_calls,
- .supertypes,
- .subtypes,
- .document_symbols,
- .workspace_symbols,
- .diagnostics,
- .workspace_diagnostics,
- .select_refs,
- .rename,
- => {},
- else => return anchor,
- },
- .errors, .pdf_links => return anchor,
- }
- if (content.len == 0) return anchor;
- const path_only = std.meta.eql(from, Origin{ .cmd = .Find });
- const trailing_newline = content[content.len - 1] == '\n';
- const body = content[0 .. content.len - @intFromBool(trailing_newline)];
- var lines = std.mem.splitScalar(u8, body, '\n');
- var previous: ?Location = null;
- var sorted = true;
- var count: usize = 0;
- while (lines.next()) |line| {
- const current: Location = if (path_only) .{ .path = line, .at = .{}, .end = line.len } else location(line);
- if (!path_only and current.at.line == 0) return anchor;
- if (previous) |last| if (last.order(current) == .gt) {
- sorted = false;
- };
- previous = current;
- count += 1;
- }
- if (sorted) return anchor;
-
- const Row = struct {
- text: []const u8,
- target: Location,
- original: usize,
-
- fn lessThan(_: void, a: @This(), b: @This()) bool {
- return switch (a.target.order(b.target)) {
- .lt => true,
- .eq => a.original < b.original,
- .gt => false,
- };
- }
- };
- const rows = try arena.alloc(Row, count);
- const copy = try arena.dupe(u8, body);
- lines = std.mem.splitScalar(u8, copy, '\n');
- for (rows, 0..) |*row, original| {
- const text = lines.next().?;
- const target: Location = if (path_only) .{ .path = text, .at = .{}, .end = text.len } else location(text);
- row.* = .{
- .text = text,
- .target = target,
- .original = original,
- };
- }
- std.mem.sort(Row, rows, {}, Row.lessThan);
- var mapped = anchor;
- var offset: usize = 0;
- for (rows, 0..) |row, i| {
- if (anchor == row.original) mapped = i;
- @memcpy(content[offset..][0..row.text.len], row.text);
- offset += row.text.len;
- if (i + 1 < rows.len or trailing_newline) {
- content[offset] = '\n';
- offset += 1;
- }
- }
- std.debug.assert(offset == content.len);
- return mapped;
- }
-
- pub fn nextResult(content: []const u8, rows: []const locations.Row, path: []const u8, at: look.Spot) usize {
- const current: Location = .{ .path = path, .at = at, .end = 0 };
- var lines = std.mem.splitScalar(u8, content, '\n');
- var first: ?usize = null;
- var row: usize = 0;
- while (lines.next()) |line| : (row += 1) {
- if (row < rows.len) {
- if (rows[row].kind != .match) continue;
- } else if (locations.isContextLine(line)) continue;
- const target = location(line);
- if (target.at.line == 0) continue;
- if (first == null) first = row;
- if (target.order(current) == .gt) return row;
- }
- return first orelse 0;
- }
-
- pub fn open(p: *Pardes, id: usize, dir: []const u8, from: Origin, arg: []const u8, content: []u8) !*Pane {
- const path = try std.fmt.allocPrint(p.gpa, "{s}/{s}", .{
- std.mem.trimEnd(u8, dir, "/"), traits(from).name,
- });
- errdefer p.gpa.free(path);
- var out: State = .{ .from = from };
- try setArg(&out, arg);
- const history = try File.History.create(p.gpa);
- errdefer p.gpa.destroy(history);
- const pane = try p.newDocPane(id);
- pane.file = .{ .path = path, .content = content, .output = out, .history = history };
- pane.cur_pinned = true;
- return pane;
- }
-
- pub fn fillResults(p: *Pardes, id: usize, dir: []const u8, from: Origin, arg: []const u8, content: []u8, initial_anchor: ?usize) !void {
- var result_content = content;
- errdefer p.gpa.free(result_content);
- var result_rows: []locations.Row = &.{};
- errdefer locations.freeRows(p.gpa, result_rows);
- const pane = p.panes[id] orelse return error.MissingPane;
- if (arg.len > max_arg) return error.ArgumentTooLong;
- var anchor = try sortResults(p.scratch.allocator(), from, content, initial_anchor);
- const by_arg = std.meta.activeTag(from) != .query;
- if (p.locations_config.context == 0 and !p.locations_config.tscontext) {
- for (p.panes, 0..) |slot, i| {
- if (i == id) continue;
- const rp = slot orelse continue;
- const rf = if (rp.file) |*f| f else continue;
- const output = if (rf.output) |*o| o else continue;
- if (!std.meta.eql(output.from, from)) continue;
- if (by_arg and !std.mem.eql(u8, output.arg(), arg)) continue;
- if (!std.mem.eql(u8, std.fs.path.dirname(rf.path) orelse "", dir)) continue;
- if (!locations.sameInput(content, rf.content, rf.location_rows, p.locations_config.layout)) continue;
- try setArg(output, arg);
- p.gpa.free(content);
- p.active = id;
- if (traits(from).steps) {
- pane.search_pane = i;
- pane.search_row = anchor;
- p.armLookWalk(i);
- }
- return;
- }
- }
- if (usesLocationsConfig(from)) {
- const formatted = try locations.format(p, dir, content, anchor, true);
- p.gpa.free(result_content);
- result_content = formatted.content;
- result_rows = formatted.rows;
- anchor = formatted.anchor;
- }
- for (p.panes, 0..) |slot, i| {
- if (i == id) continue;
- const rp = slot orelse continue;
- const rf = if (rp.file) |*f| f else continue;
- const o = if (rf.output) |*x| x else continue;
- if (!std.meta.eql(o.from, from)) continue;
- if (by_arg and !std.mem.eql(u8, o.arg(), arg)) continue;
- if (!std.mem.eql(u8, std.fs.path.dirname(rf.path) orelse "", dir)) continue;
- try setArg(o, arg);
- if (std.mem.eql(u8, rf.content, result_content)) {
- p.gpa.free(result_content);
- } else {
- File.setContent(p, rf, result_content);
- resetBody(p, rp);
- }
- if (locations.equalRows(rf.location_rows, result_rows)) {
- locations.freeRows(p.gpa, result_rows);
- } else {
- locations.freeRows(p.gpa, rf.location_rows);
- rf.location_rows = result_rows;
- if (rf.highlights.len > 0) p.tree_sitter_gpa.free(rf.highlights);
- rf.highlights = &.{};
- rf.highlight_start = 0;
- rf.syntax_dirty = true;
- }
- p.active = id;
- if (traits(from).steps) {
- pane.search_pane = i;
- pane.search_row = anchor;
- p.armLookWalk(i);
- }
- return;
- }
- const free = p.freeSlot() orelse return error.NoPaneSlots;
- const np = try open(p, free, dir, from, arg, result_content);
- np.file.?.location_rows = result_rows;
- p.placeDoc(id, free, np);
- p.active = id;
- if (traits(from).steps) {
- pane.search_pane = free;
- pane.search_row = anchor;
- p.armLookWalk(free);
- }
- }
-
- pub fn openJumps(p: *Pardes, id: usize) !void {
- var out: std.Io.Writer.Allocating = .init(p.gpa);
- errdefer out.deinit();
- for (p.jumps[0..p.njumps]) |j| {
- const jp = p.panes[j.pane] orelse continue;
- var idbuf: [16]u8 = undefined;
- const pdf_path: ?[]const u8 = if (comptime pardes.pdf_enabled) jp.pdfPath() else null;
- const has_path = if (jp.file) |f| f.output == null else pdf_path != null;
- const loc: []const u8 = if (has_path)
- (if (jp.file) |f| f.path else pdf_path.?)
- else
- std.fmt.bufPrint(&idbuf, config.pane_addr ++ "{d}", .{j.pane}) catch unreachable;
- const what: []const u8 = if (jp.file) |f|
- std.mem.trim(u8, modal.lineSlice(f.content, j.line -| 1), " \t\r")
- else if (jp.image) |iv|
- iv.path
- else if (pdf_path) |path|
- path
- else
- jp.cwdSlice();
- var cut = @min(what.len, 120);
- while (cut > 0 and cut < what.len and what[cut] & 0xc0 == 0x80) cut -= 1;
- if (j.line == 0)
- try out.writer.print("{s} {s}\n", .{ loc, what[0..cut] })
- else
- try out.writer.print("{s}:{d}:{d} {s}\n", .{ loc, j.line, j.col, what[0..cut] });
- }
- const content = try out.toOwnedSlice();
- try openStepped(p, id, .{ .cmd = .Jumplist }, content);
- }
-
- pub fn openThemes(p: *Pardes, id: usize) !void {
- var out: std.Io.Writer.Allocating = .init(p.gpa);
- errdefer out.deinit();
- try out.writer.writeAll("# Pardes themes\n");
- for (pardes.themes, 0..) |t, i| {
- if (i == pardes.native_theme_count)
- try out.writer.writeAll("\n# Legacy and imported themes\n");
- try out.writer.print(comptime config.Runtime.findAction(.theme).?.word ++ " {s}\n", .{t.name});
- }
- const content = try out.toOwnedSlice();
- try openStepped(p, id, .{ .cmd = .ThemeSel }, content);
- }
-
- pub fn isWalkRow(file: *const File.State, row: usize, line: []const u8) bool {
- const state = file.output orelse return true;
- if (usesLocationsConfig(state.from)) return isResultRow(file, row);
- if (!std.meta.eql(state.from, Origin{ .cmd = .ThemeSel })) return true;
- // Section labels are for reading, not stops in the command walk.
- return std.mem.startsWith(u8, std.mem.trimStart(u8, line, " \t"), comptime config.Runtime.findAction(.theme).?.word ++ " ");
- }
-
- pub fn openFonts(p: *Pardes, id: usize) !void {
- if (builtins.capabilities.font_picker) {
- const arena = p.scratch.allocator();
- const font_list = fonts.list(arena, null);
- var out: std.Io.Writer.Allocating = .init(p.gpa);
- errdefer out.deinit();
- for (font_list) |f|
- try out.writer.print(comptime config.Runtime.findAction(.font).?.word ++ " {s}\n", .{f.name});
- const content = try out.toOwnedSlice();
- try openStepped(p, id, .{ .cmd = .FontSel }, content);
- }
- }
-
- fn openStepped(p: *Pardes, id: usize, from: Origin, content: []u8) !void {
- errdefer p.gpa.free(content);
- const pane = p.panes[id] orelse return error.MissingPane;
- const dir = if (pane.file) |f| (std.fs.path.dirname(f.path) orelse "/") else pane.cwdSlice();
- const free = p.freeSlot() orelse return error.NoPaneSlots;
- const np = try open(p, free, dir, from, "", content);
- p.placeDoc(id, free, np);
- p.active = id;
- pane.search_pane = free;
- pane.search_row = null;
- p.armLookWalk(free);
- }
-
- fn helpContent(gpa: std.mem.Allocator, prefix: []const u8) ![]u8 {
- const full_header = "pardes builtins, and how to run each:\nSPC and its keys, a chord, a button, the\ntopbar - or the name, executed anywhere.\n\n";
- const group_header = "pardes builtins under SPC";
- const language_footer =
- "\nlanguage keys (motions, not words):\n" ++
- "gd gD gy gi gr goto: definition,\n" ++
- " declaration, type-def,\n" ++
- " implementation, refs\n" ++
- "]d [d ]D [D diagnostics: next,\n" ++
- " prev, last, first\n" ++
- "= format (applies, one\n" ++
- " undo step)\n" ++
- "Tab after a . completion, in insert\n" ++
- "C-left-click definition, by mouse\n" ++
- "SPC l ... hover, rename, symbols,\n" ++
- " calls, types: above\n";
- var len: usize = if (prefix.len == 0)
- full_header.len + language_footer.len
- else
- group_header.len + prefix.len * 2 + 2;
- for (pardes.builtin_rows) |row| {
- // a path-less builtin filters as the empty path: in the full listing
- // (which starts with nothing) and out of every group
- if (!std.mem.startsWith(u8, row.path orelse "", prefix)) continue;
- len += row.line.len + 1;
- }
- const content = try gpa.alloc(u8, len);
- var at: usize = 0;
- if (prefix.len == 0) {
- @memcpy(content[0..full_header.len], full_header);
- at = full_header.len;
- } else {
- @memcpy(content[0..group_header.len], group_header);
- at = group_header.len;
- for (prefix) |c| {
- content[at] = ' ';
- content[at + 1] = c;
- at += 2;
- }
- content[at] = '\n';
- content[at + 1] = '\n';
- at += 2;
- }
- for (pardes.builtin_rows) |row| {
- if (!std.mem.startsWith(u8, row.path orelse "", prefix)) continue;
- @memcpy(content[at..][0..row.line.len], row.line);
- at += row.line.len;
- content[at] = '\n';
- at += 1;
- }
- if (prefix.len == 0) {
- @memcpy(content[at..][0..language_footer.len], language_footer);
- at += language_footer.len;
- }
- std.debug.assert(at == content.len);
- return content;
- }
-
- pub fn openHelp(p: *Pardes, id: usize, prefix: []const u8) !void {
- const content = try helpContent(p.gpa, prefix);
- // content is handed off unfreed on purpose: openRead adopts it or frees
- // it, and nothing between the alloc above and this line can fail.
- return openRead(p, id, .{ .cmd = .Help }, prefix, content);
- }
-
- test "full Help renders every builtin row, then the language keys" {
- const content = try helpContent(std.testing.allocator, "");
- defer std.testing.allocator.free(content);
-
- // FIRST blank line: the end of the header (the footer opens with one too)
- const body = content[(std.mem.indexOf(u8, content, "\n\n") orelse
- return error.MissingHelpHeader) + 2 ..];
- var lines = std.mem.splitScalar(u8, body, '\n');
- for (pardes.builtin_rows) |row|
- try std.testing.expectEqualStrings(row.line, lines.next() orelse
- return error.MissingBuiltinHelpRow);
- // ...and after the last row, the language-keys section: the one part of
- // the keymap no builtin row can carry, closing the page.
- try std.testing.expectEqualStrings("", lines.next() orelse
- return error.MissingLanguageKeys);
- try std.testing.expectEqualStrings("language keys (motions, not words):", lines.next() orelse
- return error.MissingLanguageKeys);
- try std.testing.expect(std.mem.indexOf(u8, body, "\ngd gD gy gi gr goto: definition,\n") != null);
- // the group view stays a pure filter: no footer under a prefix
- const group = try helpContent(std.testing.allocator, "l");
- defer std.testing.allocator.free(group);
- try std.testing.expect(std.mem.indexOf(u8, group, "language keys") == null);
- }
-
- pub fn openConfig(p: *Pardes, id: usize) !void {
- var out: std.Io.Writer.Allocating = .init(p.gpa);
- errdefer out.deinit();
- var dir_buf: [1024]u8 = undefined;
- const host_fs = comptime (pardes.platform != .web and pardes.platform != .esp32p4);
- try config.Runtime.writeReport(&out.writer, .{
- .default_dump_dir = if (host_fs) (pardes.dump.defaultDirectory(&dir_buf) orelse "") else "",
- .startup_config_path = p.opts.startup_config_path,
- .platform = @tagName(pardes.platform),
- .theme_name = p.theme().name,
- .compiled_default_shell = config.default_shell,
- .gui_shader_source_mode = if (gui_shader_source_mode) |mode|
- mode.label()
- else
- null,
- .hover_delay_frames = config.look_preview_delay_frames,
- .native_images = p.native_images,
- .capabilities = builtins.capabilities,
- .state = &p.settings,
- });
- const content = try out.toOwnedSlice();
- return openRead(p, id, .{ .cmd = .Config }, "", content);
- }
-
- /// The message-row log, oldest first — the lines that were said in passing and
- /// then cleared by the next keystroke.
- pub fn openMessages(p: *Pardes, id: usize) !void {
- var out: std.Io.Writer.Allocating = .init(p.gpa);
- errdefer out.deinit();
- var i: usize = 0;
- while (p.messageLog(i)) |m| : (i += 1) {
- if (m.serial != 0) try out.writer.print("{d}: ", .{m.serial});
- try out.writer.writeAll(m.slice());
- if (m.repeats > 1) try out.writer.print(" (x{d})", .{m.repeats});
- try out.writer.writeByte('\n');
- }
- if (i == 0) try out.writer.writeAll("nothing has been said yet\n");
- const content = try out.toOwnedSlice();
- return openRead(p, id, .{ .cmd = .Messages }, "", content);
- }
-
- /// The version banner plus the embedded CHANGELOG, so an installed binary can
- /// say what it is and what changed without a repository beside it.
- pub fn openChangelog(p: *Pardes, id: usize) !void {
- var out: std.Io.Writer.Allocating = .init(p.gpa);
- errdefer out.deinit();
- try out.writer.print("pardes {s}\n\n", .{build_options.version});
- try out.writer.writeAll(@embedFile("CHANGELOG.md"));
- const content = try out.toOwnedSlice();
- return openRead(p, id, .{ .cmd = .Changelog }, "", content);
- }
-
- pub fn openEffectCode(p: *Pardes, id: usize, argument: []const u8) !void {
- const name = std.mem.trim(u8, argument, " \t\r\n");
- const setting = config.Runtime.find(name) orelse return error.UnknownEffect;
- switch (setting.action) {
- .transition, .scene => {},
- else => return error.NotAnEffect,
- }
- if (!setting.enabled(builtins.capabilities)) return error.EffectUnavailable;
- // Without -Dembed-sources the paths would name nothing this build can open.
- if (!limits.embedded_sources) return error.EffectUnavailable;
- const paths = effect_sources.forSetting(setting) orelse
- return error.EffectUnavailable;
-
- var out: std.Io.Writer.Allocating = .init(p.gpa);
- errdefer out.deinit();
- try out.writer.print("EffectCode {s} ({s})\n", .{ setting.word, @tagName(effect_sources.backend) });
- if (gui_shader_source_mode) |mode|
- try out.writer.print("GUI shader source: {s}\n", .{mode.label()});
- try out.writer.writeByte('\n');
- for (paths) |path| try out.writer.print("/virtual/{s}\n", .{path});
- const content = try out.toOwnedSlice();
- return openRead(p, id, .{ .cmd = .EffectCode }, setting.word, content);
- }
-
- pub fn openErrors(p: *Pardes, id: usize, content: []u8) !void {
- return openRead(p, id, .errors, "", content);
- }
-
- pub fn resetBody(p: *Pardes, pane: *Pane) void {
- pane.file.?.scroll = 0;
- pane.cur_row = 0;
- pane.cur_col = 0;
- pane.cur_pinned = true;
- pane.hscroll = 0;
- pane.wrap_n = 0;
- pane.msel = .{};
- pane.vsel = .{};
- pane.nsel = 0;
- pane.nsel_snap = 0;
- pane.select = false;
- pane.sel = @splat(.{});
- pane.sticky_col = -1;
- pane.append_at = null;
- pane.normal.clear();
- pane.look_at = null;
- if (p.look_hover_wait) |wait| if (wait.serial == pane.serial) {
- p.look_hover_wait = null;
- };
- if (p.look_hover_preview) |preview| if (preview.serial == pane.serial) {
- p.look_hover_preview = null;
- };
- if (p.drag == .select and p.panes[p.drag.select.id] == pane) p.drag = .none;
- }
-
- fn openRead(p: *Pardes, id: usize, from: Origin, arg: []const u8, content: []u8) !void {
- errdefer p.gpa.free(content);
- const pane = p.panes[id] orelse return error.MissingPane;
- for (p.panes, 0..) |slot, i| {
- const hp = slot orelse continue;
- const hf = if (hp.file) |*f| f else continue;
- const ho = if (hf.output) |*o| o else continue;
- if (!std.meta.eql(ho.from, from)) continue;
- try setArg(ho, arg);
- File.setContent(p, hf, content);
- resetBody(p, hp);
- p.active = i;
- return;
- }
- const dir = if (pane.file) |f| (std.fs.path.dirname(f.path) orelse "/") else pane.cwdSlice();
- const free = p.freeSlot() orelse return error.NoPaneSlots;
- const np = try open(p, free, dir, from, arg, content);
- p.placeDoc(id, free, np);
- p.active = free;
- }
-};
+pub const Output = @import("Output.zig");
pub const Mini = @import("Mini.zig");