diff options
Diffstat (limited to 'src/output_pane.zig')
| -rw-r--r-- | src/output_pane.zig | 227 |
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; } |
