From 1b52365541a7ea10fa5b334981e3128bee48ccf3 Mon Sep 17 00:00:00 2001 From: Bryan Lee <38807139+liby@users.noreply.github.com> Date: Sun, 12 Jan 2025 14:23:52 +0800 Subject: [PATCH 1/4] Add default documentation for undocumented keybind actions Previously, `ghostty +list-actions` would only show actions that had doc comments, making it difficult for users to discover all available actions. This change ensures all actions are listed with appropriate documentation. For actions without doc comments, we now generate a default message encouraging contribution. --- src/helpgen.zig | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/helpgen.zig b/src/helpgen.zig index 2084fb9f7..646f7fd47 100644 --- a/src/helpgen.zig +++ b/src/helpgen.zig @@ -115,6 +115,38 @@ fn genActions(alloc: std.mem.Allocator, writer: anytype) !void { try writer.writeAll("};\n"); } +fn genKeybindField( + alloc: std.mem.Allocator, + writer: anytype, + ast: std.zig.Ast, + comptime field: []const u8, +) !void { + const tokens = ast.tokens.items(.tag); + + // Find the field and check if it has doc comments + for (tokens, 0..) |token, i| { + if (token != .identifier) continue; + const name = ast.tokenSlice(@intCast(i)); + if (!std.mem.eql(u8, name, field)) continue; + + try writer.writeAll("pub const "); + try writer.writeAll(name); + try writer.writeAll(": [:0]const u8 = \n"); + + // If it has doc comments, use them + if (i > 0 and tokens[i - 1] == .doc_comment) { + const comment = try extractDocComments(alloc, ast, @intCast(i - 1), tokens); + try writer.writeAll(comment); + } else { + // Otherwise use default documentation + try writer.writeAll(" \\\\This action is currently undocumented.\n"); + try writer.writeAll(" \\\\Please refer to the source code or contribute documentation.\n"); + try writer.writeAll(";\n"); + } + break; + } +} + fn genKeybindActions(alloc: std.mem.Allocator, writer: anytype) !void { var ast = try std.zig.Ast.parse(alloc, @embedFile("input/Binding.zig"), .zig); defer ast.deinit(alloc); @@ -128,7 +160,7 @@ fn genKeybindActions(alloc: std.mem.Allocator, writer: anytype) !void { inline for (@typeInfo(KeybindAction).Union.fields) |field| { if (field.name[0] == '_') continue; - try genConfigField(alloc, writer, ast, field.name); + try genKeybindField(alloc, writer, ast, field.name); } try writer.writeAll("};\n"); From 05fe3e7ec3d20c26f3f1cb500ab9ee0e9a0f01f5 Mon Sep 17 00:00:00 2001 From: Bryan Lee <38807139+liby@users.noreply.github.com> Date: Sun, 12 Jan 2025 19:54:51 +0800 Subject: [PATCH 2/4] Ensure last action's documentation is properly generated The issue was caused by the documentation generation logic not writing the final buffered content. --- src/build/webgen/main_actions.zig | 7 +++++++ src/input/Binding.zig | 6 +++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/build/webgen/main_actions.zig b/src/build/webgen/main_actions.zig index f4dffbc13..e802aac4a 100644 --- a/src/build/webgen/main_actions.zig +++ b/src/build/webgen/main_actions.zig @@ -50,9 +50,16 @@ pub fn genKeybindActions(writer: anytype) !void { '\n', ); while (iter.next()) |s| { + // If it is the last line and empty, then skip it. + if (iter.peek() == null and s.len == 0) continue; try buffer.appendSlice(s); try buffer.appendSlice("\n"); } } } + + // Write any remaining buffered documentation + if (buffer.items.len > 0) { + try writer.writeAll(buffer.items); + } } diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 48725fb13..f29030bde 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -236,9 +236,9 @@ pub const Action = union(enum) { /// Send an `ESC` sequence. esc: []const u8, - // Send the given text. Uses Zig string literal syntax. This is currently - // not validated. If the text is invalid (i.e. contains an invalid escape - // sequence), the error will currently only show up in logs. + /// Send the given text. Uses Zig string literal syntax. This is currently + /// not validated. If the text is invalid (i.e. contains an invalid escape + /// sequence), the error will currently only show up in logs. text: []const u8, /// Send data to the pty depending on whether cursor key mode is enabled From 8e2c55a5dab4a4be18ca74f8f3895137fd73d8d0 Mon Sep 17 00:00:00 2001 From: Bryan Lee <38807139+liby@users.noreply.github.com> Date: Mon, 13 Jan 2025 00:20:28 +0800 Subject: [PATCH 3/4] Improve `list-actions` command documentation formatting This commit fixes two issues with the `list-actions` command: 1. Ensures all actions are listed, including those without individual documentation but sharing docs with related actions 2. Improves documentation formatting with proper indentation and grouping --- src/cli/list_actions.zig | 50 +++++++++++++++++++++++++++++++--------- src/helpgen.zig | 34 +-------------------------- 2 files changed, 40 insertions(+), 44 deletions(-) diff --git a/src/cli/list_actions.zig b/src/cli/list_actions.zig index 65b9dcdad..e4b22023d 100644 --- a/src/cli/list_actions.zig +++ b/src/cli/list_actions.zig @@ -3,6 +3,7 @@ const args = @import("args.zig"); const Action = @import("action.zig").Action; const Allocator = std.mem.Allocator; const help_strings = @import("help_strings"); +const KeybindAction = @import("../input/Binding.zig").Action; pub const Options = struct { /// If `true`, print out documentation about the action associated with the @@ -36,18 +37,45 @@ pub fn run(alloc: Allocator) !u8 { } const stdout = std.io.getStdOut().writer(); - const info = @typeInfo(help_strings.KeybindAction); - inline for (info.Struct.decls) |field| { - try stdout.print("{s}", .{field.name}); - if (opts.docs) { - try stdout.print(":\n", .{}); - var iter = std.mem.splitScalar(u8, std.mem.trimRight(u8, @field(help_strings.KeybindAction, field.name), &std.ascii.whitespace), '\n'); - while (iter.next()) |line| { - try stdout.print(" {s}\n", .{line}); - } - } else { - try stdout.print("\n", .{}); + + var buffer = std.ArrayList(u8).init(std.heap.page_allocator); + defer buffer.deinit(); + + const fields = @typeInfo(KeybindAction).Union.fields; + inline for (fields) |field| { + if (field.name[0] == '_') continue; + + // Write previously stored doc comment below all related actions + if (@hasDecl(help_strings.KeybindAction, field.name)) { + try stdout.writeAll(buffer.items); + try stdout.writeAll("\n"); + + buffer.clearRetainingCapacity(); } + + // Write the field name. + try stdout.writeAll(field.name); + try stdout.writeAll(":\n"); + + if (@hasDecl(help_strings.KeybindAction, field.name)) { + var iter = std.mem.splitScalar( + u8, + @field(help_strings.KeybindAction, field.name), + '\n', + ); + while (iter.next()) |s| { + // If it is the last line and empty, then skip it. + if (iter.peek() == null and s.len == 0) continue; + try buffer.appendSlice(" "); + try buffer.appendSlice(s); + try buffer.appendSlice("\n"); + } + } + } + + // Write any remaining buffered documentation + if (buffer.items.len > 0) { + try stdout.writeAll(buffer.items); } return 0; diff --git a/src/helpgen.zig b/src/helpgen.zig index 646f7fd47..2084fb9f7 100644 --- a/src/helpgen.zig +++ b/src/helpgen.zig @@ -115,38 +115,6 @@ fn genActions(alloc: std.mem.Allocator, writer: anytype) !void { try writer.writeAll("};\n"); } -fn genKeybindField( - alloc: std.mem.Allocator, - writer: anytype, - ast: std.zig.Ast, - comptime field: []const u8, -) !void { - const tokens = ast.tokens.items(.tag); - - // Find the field and check if it has doc comments - for (tokens, 0..) |token, i| { - if (token != .identifier) continue; - const name = ast.tokenSlice(@intCast(i)); - if (!std.mem.eql(u8, name, field)) continue; - - try writer.writeAll("pub const "); - try writer.writeAll(name); - try writer.writeAll(": [:0]const u8 = \n"); - - // If it has doc comments, use them - if (i > 0 and tokens[i - 1] == .doc_comment) { - const comment = try extractDocComments(alloc, ast, @intCast(i - 1), tokens); - try writer.writeAll(comment); - } else { - // Otherwise use default documentation - try writer.writeAll(" \\\\This action is currently undocumented.\n"); - try writer.writeAll(" \\\\Please refer to the source code or contribute documentation.\n"); - try writer.writeAll(";\n"); - } - break; - } -} - fn genKeybindActions(alloc: std.mem.Allocator, writer: anytype) !void { var ast = try std.zig.Ast.parse(alloc, @embedFile("input/Binding.zig"), .zig); defer ast.deinit(alloc); @@ -160,7 +128,7 @@ fn genKeybindActions(alloc: std.mem.Allocator, writer: anytype) !void { inline for (@typeInfo(KeybindAction).Union.fields) |field| { if (field.name[0] == '_') continue; - try genKeybindField(alloc, writer, ast, field.name); + try genConfigField(alloc, writer, ast, field.name); } try writer.writeAll("};\n"); From 0016199ec3c6806b2db185060062138dddf3af14 Mon Sep 17 00:00:00 2001 From: Bryan Lee <38807139+liby@users.noreply.github.com> Date: Mon, 13 Jan 2025 11:20:00 +0800 Subject: [PATCH 4/4] Extract keybind actions help generation into a dedicated module --- src/build/webgen/main_actions.zig | 61 +---------------- src/cli/list_actions.zig | 44 +----------- src/input/helpgen_actions.zig | 107 ++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 101 deletions(-) create mode 100644 src/input/helpgen_actions.zig diff --git a/src/build/webgen/main_actions.zig b/src/build/webgen/main_actions.zig index e802aac4a..65420d7e2 100644 --- a/src/build/webgen/main_actions.zig +++ b/src/build/webgen/main_actions.zig @@ -1,65 +1,8 @@ const std = @import("std"); const help_strings = @import("help_strings"); -const KeybindAction = @import("../../input/Binding.zig").Action; +const helpgen_actions = @import("../../helpgen_actions.zig"); pub fn main() !void { const output = std.io.getStdOut().writer(); - try genKeybindActions(output); -} - -pub fn genKeybindActions(writer: anytype) !void { - // Write the header - try writer.writeAll( - \\--- - \\title: Keybinding Action Reference - \\description: Reference of all Ghostty keybinding actions. - \\editOnGithubLink: https://github.com/ghostty-org/ghostty/edit/main/src/input/Binding.zig - \\--- - \\ - \\This is a reference of all Ghostty keybinding actions. - \\ - \\ - ); - - @setEvalBranchQuota(5_000); - - var buffer = std.ArrayList(u8).init(std.heap.page_allocator); - defer buffer.deinit(); - - const fields = @typeInfo(KeybindAction).Union.fields; - inline for (fields) |field| { - if (field.name[0] == '_') continue; - - // Write previously stored doc comment below all related actions - if (@hasDecl(help_strings.KeybindAction, field.name)) { - try writer.writeAll(buffer.items); - try writer.writeAll("\n"); - - buffer.clearRetainingCapacity(); - } - - // Write the field name. - try writer.writeAll("## `"); - try writer.writeAll(field.name); - try writer.writeAll("`\n"); - - if (@hasDecl(help_strings.KeybindAction, field.name)) { - var iter = std.mem.splitScalar( - u8, - @field(help_strings.KeybindAction, field.name), - '\n', - ); - while (iter.next()) |s| { - // If it is the last line and empty, then skip it. - if (iter.peek() == null and s.len == 0) continue; - try buffer.appendSlice(s); - try buffer.appendSlice("\n"); - } - } - } - - // Write any remaining buffered documentation - if (buffer.items.len > 0) { - try writer.writeAll(buffer.items); - } + try helpgen_actions.generate(output, .markdown, std.heap.page_allocator); } diff --git a/src/cli/list_actions.zig b/src/cli/list_actions.zig index e4b22023d..e6c34adb2 100644 --- a/src/cli/list_actions.zig +++ b/src/cli/list_actions.zig @@ -2,8 +2,7 @@ const std = @import("std"); const args = @import("args.zig"); const Action = @import("action.zig").Action; const Allocator = std.mem.Allocator; -const help_strings = @import("help_strings"); -const KeybindAction = @import("../input/Binding.zig").Action; +const helpgen_actions = @import("../helpgen_actions.zig"); pub const Options = struct { /// If `true`, print out documentation about the action associated with the @@ -37,46 +36,7 @@ pub fn run(alloc: Allocator) !u8 { } const stdout = std.io.getStdOut().writer(); - - var buffer = std.ArrayList(u8).init(std.heap.page_allocator); - defer buffer.deinit(); - - const fields = @typeInfo(KeybindAction).Union.fields; - inline for (fields) |field| { - if (field.name[0] == '_') continue; - - // Write previously stored doc comment below all related actions - if (@hasDecl(help_strings.KeybindAction, field.name)) { - try stdout.writeAll(buffer.items); - try stdout.writeAll("\n"); - - buffer.clearRetainingCapacity(); - } - - // Write the field name. - try stdout.writeAll(field.name); - try stdout.writeAll(":\n"); - - if (@hasDecl(help_strings.KeybindAction, field.name)) { - var iter = std.mem.splitScalar( - u8, - @field(help_strings.KeybindAction, field.name), - '\n', - ); - while (iter.next()) |s| { - // If it is the last line and empty, then skip it. - if (iter.peek() == null and s.len == 0) continue; - try buffer.appendSlice(" "); - try buffer.appendSlice(s); - try buffer.appendSlice("\n"); - } - } - } - - // Write any remaining buffered documentation - if (buffer.items.len > 0) { - try stdout.writeAll(buffer.items); - } + try helpgen_actions.generate(stdout, .plaintext, std.heap.page_allocator); return 0; } diff --git a/src/input/helpgen_actions.zig b/src/input/helpgen_actions.zig new file mode 100644 index 000000000..9a7612b57 --- /dev/null +++ b/src/input/helpgen_actions.zig @@ -0,0 +1,107 @@ +//! This module is a help generator for keybind actions documentation. +//! It can generate documentation in different formats (plaintext for CLI, +//! markdown for website) while maintaining consistent content. + +const std = @import("std"); +const KeybindAction = @import("Binding.zig").Action; +const help_strings = @import("help_strings"); + +/// Format options for generating keybind actions documentation +pub const Format = enum { + /// Plain text output with indentation + plaintext, + /// Markdown formatted output + markdown, + + fn formatFieldName(self: Format, writer: anytype, field_name: []const u8) !void { + switch (self) { + .plaintext => { + try writer.writeAll(field_name); + try writer.writeAll(":\n"); + }, + .markdown => { + try writer.writeAll("## `"); + try writer.writeAll(field_name); + try writer.writeAll("`\n"); + }, + } + } + + fn formatDocLine(self: Format, writer: anytype, line: []const u8) !void { + switch (self) { + .plaintext => { + try writer.appendSlice(" "); + try writer.appendSlice(line); + try writer.appendSlice("\n"); + }, + .markdown => { + try writer.appendSlice(line); + try writer.appendSlice("\n"); + }, + } + } + + fn header(self: Format) ?[]const u8 { + return switch (self) { + .plaintext => null, + .markdown => + \\--- + \\title: Keybinding Action Reference + \\description: Reference of all Ghostty keybinding actions. + \\editOnGithubLink: https://github.com/ghostty-org/ghostty/edit/main/src/input/Binding.zig + \\--- + \\ + \\This is a reference of all Ghostty keybinding actions. + \\ + \\ + , + }; + } +}; + +/// Generate keybind actions documentation with the specified format +pub fn generate( + writer: anytype, + format: Format, + page_allocator: std.mem.Allocator, +) !void { + if (format.header()) |header| { + try writer.writeAll(header); + } + + var buffer = std.ArrayList(u8).init(page_allocator); + defer buffer.deinit(); + + const fields = @typeInfo(KeybindAction).Union.fields; + inline for (fields) |field| { + if (field.name[0] == '_') continue; + + // Write previously stored doc comment below all related actions + if (@hasDecl(help_strings.KeybindAction, field.name)) { + try writer.writeAll(buffer.items); + try writer.writeAll("\n"); + + buffer.clearRetainingCapacity(); + } + + try format.formatFieldName(writer, field.name); + + if (@hasDecl(help_strings.KeybindAction, field.name)) { + var iter = std.mem.splitScalar( + u8, + @field(help_strings.KeybindAction, field.name), + '\n', + ); + while (iter.next()) |s| { + // If it is the last line and empty, then skip it. + if (iter.peek() == null and s.len == 0) continue; + try format.formatDocLine(&buffer, s); + } + } + } + + // Write any remaining buffered documentation + if (buffer.items.len > 0) { + try writer.writeAll(buffer.items); + } +}