diff options
Diffstat (limited to 'src/panes.zig')
| -rw-r--r-- | src/panes.zig | 766 |
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"); |
