summaryrefslogtreecommitdiff
path: root/src/output_pane.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/output_pane.zig')
-rw-r--r--src/output_pane.zig227
1 files changed, 119 insertions, 108 deletions
diff --git a/src/output_pane.zig b/src/output_pane.zig
index 1bf14b7c..7b2d12d9 100644
--- a/src/output_pane.zig
+++ b/src/output_pane.zig
@@ -22,7 +22,7 @@ const File = pardes.File;
const file_pane = @import("file_pane.zig");
const modal = @import("modal.zig");
const builtins = @import("builtins.zig");
-const Builtin = builtins.Builtin();
+const Builtin = builtins.registry.Builtin();
const config = @import("config.zig");
const lsp = @import("lsp/lsp.zig");
/// the installed fonts, for openFonts. GUI only, behind the same comptime
@@ -122,40 +122,15 @@ 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 },
- .cmd => |b| switch (b) {
- // The builtin index. Neither steppable nor executable, and that is
- // a decision rather than an omission: its rows are not locations
- // (a look on `SPC` would go looking for a file called SPC), and
- // `executes` — which is exactly what makes ThemeSel a picker —
- // would mean walking the list runs Kill, Del and Delcol in turn.
- // Wearing a theme is free; wearing Kill ends the session. The
- // names in it are still live text, so middle-clicking the ONE you
- // want does the picking, which is all a picker was for.
- .Help => .{ .name = config.help_buffer },
- // the focus history, one location per row: not a search, but the
- // same kind of list, so n/N walk it and a row is a look target
- .Jumplist => .{ .name = config.jumps_buffer, .steps = true },
- // the theme ring, one `Theme <name>` per row. The only buffer whose
- // rows are COMMANDS rather than locations, so n/N execute them:
- // walking the list wears each theme, and stopping is picking one.
- .ThemeSel => .{ .name = config.themes_buffer, .steps = true, .executes = true },
- // the font picker, the theme picker one layer down: same rows,
- // same verb, its own name. It is a BRANCH and not a prong because
- // FontSel only exists in a gui build (see builtins.zig) — a prong
- // naming an enum field the tty fold does not have is a compile
- // error, and a comptime-false `if` is the one form that is not
- // even analysed there.
- //
- // Find (file names), Grep (file contents), and PdfSections all
- // list lookable locations. Feature-only rows remain branches so
- // builds whose Builtin enum omits them never analyse the field.
- else => blk: {
- if (pardes.platform == .gui) if (b == .FontSel)
- break :blk .{ .name = config.fonts_buffer, .steps = true, .executes = true };
- if (pardes.pdf_enabled) if (b == .PdfSections)
- break :blk .{ .name = config.pdf_sections_buffer, .steps = true };
+ .cmd => |b| blk: {
+ const meta = builtins.registry.outputTraits(b) orelse
break :blk .{ .name = config.search_buffer, .steps = true };
- },
+ break :blk .{
+ .name = meta.name,
+ .steps = meta.steps,
+ .jumps = meta.jumps,
+ .executes = meta.executes,
+ };
},
.query => |k| switch (k) {
.hover => .{ .name = config.hover_buffer },
@@ -232,6 +207,7 @@ pub fn open(p: *Pardes, id: usize, dir: []const u8, from: Origin, arg: []const u
var out: Output = .{ .from = from };
setArg(&out, arg);
pane.file = .{ .path = path, .content = content, .output = out };
+ pane.kind = .file;
pane.cur_pinned = true;
return pane;
}
@@ -257,11 +233,9 @@ pub fn open(p: *Pardes, id: usize, dir: []const u8, from: Origin, arg: []const u
///
/// `content` is gpa-owned: adopted by the buffer, or freed here when there is
/// nowhere to put it. `anchor` is the row n/N step from, null for the top.
-pub fn fillResults(p: *Pardes, id: usize, dir: []const u8, from: Origin, arg: []const u8, content: []u8, anchor: ?usize) void {
- const pane = p.panes[id] orelse {
- p.gpa.free(content);
- return;
- };
+pub fn fillResults(p: *Pardes, id: usize, dir: []const u8, from: Origin, arg: []const u8, content: []u8, anchor: ?usize) !void {
+ errdefer p.gpa.free(content);
+ const pane = p.panes[id] orelse return error.MissingPane;
const by_arg = std.meta.activeTag(from) != .query;
for (p.panes, 0..) |slot, i| {
if (i == id) continue;
@@ -286,14 +260,8 @@ pub fn fillResults(p: *Pardes, id: usize, dir: []const u8, from: Origin, arg: []
}
return;
}
- const free = p.freeSlot() orelse {
- p.gpa.free(content);
- return;
- };
- const np = open(p, free, dir, from, arg, content) catch {
- p.gpa.free(content);
- return;
- };
+ 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 = id;
// prose is not a list of locations: n/N over a hover blurb would step to
@@ -305,6 +273,11 @@ pub fn fillResults(p: *Pardes, id: usize, dir: []const u8, from: Origin, arg: []
}
}
+fn putPrint(dst: []u8, at: *usize, comptime fmt: []const u8, args: anytype) void {
+ const text = std.fmt.bufPrint(dst[at.*..], fmt, args) catch unreachable;
+ at.* += text.len;
+}
+
/// The Jumplist builtin: the focus history (Pardes.jumps) written out as text,
/// one row per location, oldest first — the same `location text` shape every
/// results buffer here has, which is what buys n/N stepping and Look-on-a-row
@@ -322,9 +295,9 @@ pub fn fillResults(p: *Pardes, id: usize, dir: []const u8, from: Origin, arg: []
/// image — has no file to point at and gets `@pN`. The trailing text is the
/// content line for anything holding text, else the pane's directory: enough
/// to recognise the place without opening it.
-pub fn openJumps(p: *Pardes, id: usize) void {
+pub fn openJumps(p: *Pardes, id: usize) !void {
const arena = p.scratch.allocator();
- var out: std.ArrayList(u8) = .empty;
+ var len: usize = 0;
for (p.jumps[0..p.njumps]) |j| {
const jp = p.panes[j.pane] orelse continue;
var idbuf: [16]u8 = undefined;
@@ -333,7 +306,7 @@ pub fn openJumps(p: *Pardes, id: usize) void {
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 continue;
+ 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|
@@ -342,21 +315,42 @@ pub fn openJumps(p: *Pardes, id: usize) void {
path
else
jp.cwdSlice();
- // one line of a file can be the whole file: cut it, but never mid
- // codepoint — a partial UTF-8 sequence reaches the renderer as a row
- // and there is nothing sane for it to draw (grepText cuts the same way)
var cut = @min(what.len, 120);
while (cut > 0 and cut < what.len and what[cut] & 0xc0 == 0x80) cut -= 1;
- // line 0 is a place with no spot in it (a shell whose cursor is still
- // the program's): it is written WITHOUT the suffix, which is the same
- // thing a bare path has always meant to a look.
- const row = if (j.line == 0)
- std.fmt.allocPrint(arena, "{s} {s}\n", .{ loc, what[0..cut] }) catch return
+ len += if (j.line == 0)
+ std.fmt.count("{s} {s}\n", .{ loc, what[0..cut] })
else
- std.fmt.allocPrint(arena, "{s}:{d}:{d} {s}\n", .{ loc, j.line, j.col, what[0..cut] }) catch return;
- out.appendSlice(arena, row) catch return;
+ std.fmt.count("{s}:{d}:{d} {s}\n", .{ loc, j.line, j.col, what[0..cut] });
}
- openStepped(p, id, .{ .cmd = .Jumplist }, out.items);
+
+ const out = try arena.alloc(u8, len);
+ var at: usize = 0;
+ 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)
+ putPrint(out, &at, "{s} {s}\n", .{ loc, what[0..cut] })
+ else
+ putPrint(out, &at, "{s}:{d}:{d} {s}\n", .{ loc, j.line, j.col, what[0..cut] });
+ }
+ std.debug.assert(at == out.len);
+ try openStepped(p, id, .{ .cmd = .Jumplist }, out);
}
/// The ThemeSel builtin: the theme ring written out as one `Theme <name>` row
@@ -369,13 +363,17 @@ pub fn openJumps(p: *Pardes, id: usize) void {
/// The command's own name comes from the builtin rather than a literal: the
/// word is derived from the struct in exactly one place (builtins.word), and a
/// rename there must not leave rows here that name something that is gone.
-pub fn openThemes(p: *Pardes, id: usize) void {
+pub fn openThemes(p: *Pardes, id: usize) !void {
const arena = p.scratch.allocator();
- var out: std.ArrayList(u8) = .empty;
- for (pardes.themes) |t| {
- out.print(arena, comptime builtins.word(builtins.Theme) ++ " {s}\n", .{t.name}) catch return;
- }
- openStepped(p, id, .{ .cmd = .ThemeSel }, out.items);
+ var len: usize = 0;
+ for (pardes.themes) |t|
+ len += std.fmt.count(comptime builtins.word(builtins.Theme) ++ " {s}\n", .{t.name});
+ const out = try arena.alloc(u8, len);
+ var at: usize = 0;
+ for (pardes.themes) |t|
+ putPrint(out, &at, comptime builtins.word(builtins.Theme) ++ " {s}\n", .{t.name});
+ std.debug.assert(at == out.len);
+ try openStepped(p, id, .{ .cmd = .ThemeSel }, out);
}
/// The FontSel builtin: openThemes over the fonts installed on the machine
@@ -388,14 +386,19 @@ pub fn openThemes(p: *Pardes, id: usize) void {
/// the FontSel origin both exist only there, and a comptime-false `if` is what
/// keeps the tty build from analysing either. The dead parameters on that
/// build are the honest shape of "this platform cannot open one".
-pub fn openFonts(p: *Pardes, id: usize) void {
+pub fn openFonts(p: *Pardes, id: usize) !void {
if (pardes.platform == .gui) {
const arena = p.scratch.allocator();
- var out: std.ArrayList(u8) = .empty;
- for (fonts.list(arena, null)) |f| {
- out.print(arena, comptime builtins.word(builtins.Font) ++ " {s}\n", .{f.name}) catch return;
- }
- openStepped(p, id, .{ .cmd = .FontSel }, out.items);
+ const font_list = fonts.list(arena, null);
+ var len: usize = 0;
+ for (font_list) |f|
+ len += std.fmt.count(comptime builtins.word(builtins.Font) ++ " {s}\n", .{f.name});
+ const out = try arena.alloc(u8, len);
+ var at: usize = 0;
+ for (font_list) |f|
+ putPrint(out, &at, comptime builtins.word(builtins.Font) ++ " {s}\n", .{f.name});
+ std.debug.assert(at == out.len);
+ try openStepped(p, id, .{ .cmd = .FontSel }, out);
}
}
@@ -405,18 +408,13 @@ pub fn openFonts(p: *Pardes, id: usize) void {
///
/// Focus stays with the pane that ASKED, exactly as it does after a search:
/// n/N are read there, and they step the buffer they just armed.
-fn openStepped(p: *Pardes, id: usize, from: Origin, text: []const u8) void {
- const pane = p.panes[id] orelse return;
- const content = p.gpa.dupe(u8, text) catch return;
+fn openStepped(p: *Pardes, id: usize, from: Origin, text: []const u8) !void {
+ const pane = p.panes[id] orelse return error.MissingPane;
+ const content = try p.gpa.dupe(u8, text);
+ errdefer p.gpa.free(content);
const dir = if (pane.file) |f| (std.fs.path.dirname(f.path) orelse "/") else pane.cwdSlice();
- const free = p.freeSlot() orelse {
- p.gpa.free(content);
- return;
- };
- const np = open(p, free, dir, from, "", content) catch {
- p.gpa.free(content);
- return;
- };
+ 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;
@@ -435,28 +433,46 @@ fn openStepped(p: *Pardes, id: usize, from: Origin, text: []const u8) void {
/// SPC cannot reach at all, so the reference page IS the filter's degenerate
/// case. A second builtin would have been a second renderer over a superset of
/// these rows, and the two would have drifted the first time a column moved.
-pub fn openHelp(p: *Pardes, id: usize, prefix: []const u8) void {
- const pane = p.panes[id] orelse return;
- const arena = p.scratch.allocator();
- var out: std.ArrayList(u8) = .empty;
+pub fn openHelp(p: *Pardes, id: usize, prefix: []const u8) !void {
+ const pane = p.panes[id] orelse return error.MissingPane;
+ 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";
+ var len: usize = if (prefix.len == 0)
+ full_header.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 p.gpa.alloc(u8, len);
+ errdefer p.gpa.free(content);
+ var at: usize = 0;
if (prefix.len == 0) {
- // the header names the four KINDS of shortcut a row's columns can
- // hold, because a blank column is only readable once you know what
- // would have been in it
- out.appendSlice(arena, "pardes builtins, and how to run each:\nSPC and its keys, a chord, a button, the\ntopbar - or the name, executed anywhere.\n\n") catch return;
+ @memcpy(content[0..full_header.len], full_header);
+ at = full_header.len;
} else {
- out.appendSlice(arena, "pardes builtins under SPC") catch return;
- for (prefix) |c| out.appendSlice(arena, &[_]u8{ ' ', c }) catch return;
- out.appendSlice(arena, "\n\n") catch return;
+ @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| {
- // 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;
- out.appendSlice(arena, row.line) catch return;
- out.append(arena, '\n') catch return;
+ @memcpy(content[at..][0..row.line.len], row.line);
+ at += row.line.len;
+ content[at] = '\n';
+ at += 1;
}
- const content = p.gpa.dupe(u8, out.items) catch return;
+ std.debug.assert(at == content.len);
// the buffer says what made it, so finding the open one is asking that and
// not matching its name
for (p.panes, 0..) |slot, i| {
@@ -473,13 +489,8 @@ pub fn openHelp(p: *Pardes, id: usize, prefix: []const u8) void {
return;
}
const dir = if (pane.file) |f| (std.fs.path.dirname(f.path) orelse "/") else pane.cwdSlice();
- const free = p.freeSlot() orelse {
- p.gpa.free(content);
- return;
- };
- const np = open(p, free, dir, .{ .cmd = .Help }, prefix, content) catch {
- p.gpa.free(content);
- return;
- };
+ const free = p.freeSlot() orelse return error.NoPaneSlots;
+ const np = try open(p, free, dir, .{ .cmd = .Help }, prefix, content);
p.placeDoc(id, free, np);
+ p.active = free;
}