libghostty: selection word/line/output/all helpers

This commit is contained in:
Mitchell Hashimoto
2026-05-24 12:53:41 -07:00
parent 847b8afc87
commit cc48312c08
9 changed files with 405 additions and 60 deletions

View File

@@ -9,6 +9,7 @@
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <ghostty/vt/grid_ref.h>
#include <ghostty/vt/point.h>
@@ -78,6 +79,57 @@ typedef struct {
bool rectangle;
} GhosttySelection;
/**
* Options for deriving a word selection from a terminal grid reference.
*
* This is a sized struct. Use GHOSTTY_INIT_SIZED() to initialize it.
* If boundary_codepoints is NULL and boundary_codepoints_len is 0, Ghostty's
* default word-boundary codepoints are used. If boundary_codepoints_len is
* non-zero, boundary_codepoints must not be NULL.
*
* @ingroup selection
*/
typedef struct {
/** Size of this struct in bytes. Must be set to sizeof(GhosttyTerminalSelectWordOptions). */
size_t size;
/** Grid reference under which to derive the word selection. */
GhosttyGridRef ref;
/** Optional word-boundary codepoints as uint32_t scalar values. */
const uint32_t* boundary_codepoints;
/** Number of entries in boundary_codepoints. */
size_t boundary_codepoints_len;
} GhosttyTerminalSelectWordOptions;
/**
* Options for deriving a line selection from a terminal grid reference.
*
* This is a sized struct. Use GHOSTTY_INIT_SIZED() to initialize it.
* If whitespace is NULL and whitespace_len is 0, Ghostty's default line-trim
* whitespace codepoints are used. If whitespace_len is non-zero, whitespace
* must not be NULL.
*
* @ingroup selection
*/
typedef struct {
/** Size of this struct in bytes. Must be set to sizeof(GhosttyTerminalSelectLineOptions). */
size_t size;
/** Grid reference under which to derive the line selection. */
GhosttyGridRef ref;
/** Optional codepoints to trim from the start and end of the line. */
const uint32_t* whitespace;
/** Number of entries in whitespace. */
size_t whitespace_len;
/** Whether semantic prompt state changes should bound the line selection. */
bool semantic_prompt_boundary;
} GhosttyTerminalSelectLineOptions;
/**
* Ordering of a selection's endpoints in terminal coordinates.
*
@@ -158,6 +210,86 @@ typedef enum GHOSTTY_ENUM_TYPED {
GHOSTTY_SELECTION_ADJUST_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttySelectionAdjust;
/**
* Derive a word selection snapshot from a terminal grid reference.
*
* The returned selection is not installed as the terminal's current
* selection. It is a snapshot with the same lifetime rules as GhosttySelection.
*
* @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
* @param options Word-selection options
* @param[out] out_selection On success, receives the derived selection
* @return GHOSTTY_SUCCESS on success, GHOSTTY_NO_VALUE if the valid ref has
* no selectable word content, or GHOSTTY_INVALID_VALUE if the
* terminal, options, ref, codepoint pointer, or output pointer are
* invalid.
*
* @ingroup selection
*/
GHOSTTY_API GhosttyResult ghostty_terminal_select_word(
GhosttyTerminal terminal,
const GhosttyTerminalSelectWordOptions* options,
GhosttySelection* out_selection);
/**
* Derive a line selection snapshot from a terminal grid reference.
*
* The returned selection is not installed as the terminal's current
* selection. It is a snapshot with the same lifetime rules as GhosttySelection.
*
* @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
* @param options Line-selection options
* @param[out] out_selection On success, receives the derived selection
* @return GHOSTTY_SUCCESS on success, GHOSTTY_NO_VALUE if the valid ref has
* no selectable line content, or GHOSTTY_INVALID_VALUE if the
* terminal, options, ref, codepoint pointer, or output pointer are
* invalid.
*
* @ingroup selection
*/
GHOSTTY_API GhosttyResult ghostty_terminal_select_line(
GhosttyTerminal terminal,
const GhosttyTerminalSelectLineOptions* options,
GhosttySelection* out_selection);
/**
* Derive a selection snapshot covering all selectable terminal content.
*
* The returned selection is not installed as the terminal's current
* selection. It is a snapshot with the same lifetime rules as GhosttySelection.
*
* @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
* @param[out] out_selection On success, receives the derived selection
* @return GHOSTTY_SUCCESS on success, GHOSTTY_NO_VALUE if there is no
* selectable content, or GHOSTTY_INVALID_VALUE if the terminal or
* output pointer is invalid.
*
* @ingroup selection
*/
GHOSTTY_API GhosttyResult ghostty_terminal_select_all(
GhosttyTerminal terminal,
GhosttySelection* out_selection);
/**
* Derive a command-output selection snapshot from a terminal grid reference.
*
* The returned selection is not installed as the terminal's current
* selection. It is a snapshot with the same lifetime rules as GhosttySelection.
*
* @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
* @param ref Grid reference within command output to select
* @param[out] out_selection On success, receives the derived selection
* @return GHOSTTY_SUCCESS on success, GHOSTTY_NO_VALUE if the valid ref is
* not selectable command output, or GHOSTTY_INVALID_VALUE if the
* terminal, ref, or output pointer is invalid.
*
* @ingroup selection
*/
GHOSTTY_API GhosttyResult ghostty_terminal_select_output(
GhosttyTerminal terminal,
GhosttyGridRef ref,
GhosttySelection* out_selection);
/**
* Adjust a selection snapshot using terminal selection semantics.
*

View File

@@ -49,6 +49,7 @@ const string = @import("string.zig");
const terminal = struct {
const CursorStyle = @import("../terminal/cursor.zig").Style;
const color = @import("../terminal/color.zig");
const selection_codepoints = @import("../terminal/selection_codepoints.zig");
const style = @import("../terminal/style.zig");
const x11_color = @import("../terminal/x11_color.zig");
};
@@ -6149,32 +6150,8 @@ pub const RepeatableString = struct {
pub const SelectionWordChars = struct {
const Self = @This();
/// Default boundary characters: ` \t'"│`|:;,()[]{}<>$`
const default_codepoints = [_]u21{
0, // null
' ', // space
'\t', // tab
'\'', // single quote
'"', // double quote
'│', // U+2502 box drawing
'`', // backtick
'|', // pipe
':', // colon
';', // semicolon
',', // comma
'(', // left paren
')', // right paren
'[', // left bracket
']', // right bracket
'{', // left brace
'}', // right brace
'<', // less than
'>', // greater than
'$', // dollar
};
/// The parsed codepoints. Always includes null (U+0000) at index 0.
codepoints: []const u21 = &default_codepoints,
codepoints: []const u21 = &terminal.selection_codepoints.default_word_boundaries,
pub fn parseCLI(self: *Self, alloc: Allocator, input: ?[]const u8) !void {
const value = input orelse return error.ValueRequired;

View File

@@ -239,6 +239,10 @@ comptime {
@export(&c.terminal_mode_set, .{ .name = "ghostty_terminal_mode_set" });
@export(&c.terminal_get, .{ .name = "ghostty_terminal_get" });
@export(&c.terminal_get_multi, .{ .name = "ghostty_terminal_get_multi" });
@export(&c.terminal_select_word, .{ .name = "ghostty_terminal_select_word" });
@export(&c.terminal_select_line, .{ .name = "ghostty_terminal_select_line" });
@export(&c.terminal_select_all, .{ .name = "ghostty_terminal_select_all" });
@export(&c.terminal_select_output, .{ .name = "ghostty_terminal_select_output" });
@export(&c.terminal_selection_adjust, .{ .name = "ghostty_terminal_selection_adjust" });
@export(&c.terminal_selection_order, .{ .name = "ghostty_terminal_selection_order" });
@export(&c.terminal_selection_ordered, .{ .name = "ghostty_terminal_selection_ordered" });

View File

@@ -13,6 +13,7 @@ const tripwire = @import("../tripwire.zig");
const unicode = @import("../unicode/main.zig");
const Selection = @import("Selection.zig");
const PageList = @import("PageList.zig");
const selection_codepoints = @import("selection_codepoints.zig");
const StringMap = @import("StringMap.zig");
const ScreenFormatter = @import("formatter.zig").ScreenFormatter;
const osc = @import("osc.zig");
@@ -2516,7 +2517,7 @@ pub const SelectLine = struct {
/// These are the codepoints to consider whitespace to trim
/// from the ends of the selection.
whitespace: ?[]const u21 = &.{ 0, ' ', '\t' },
whitespace: ?[]const u21 = &selection_codepoints.default_line_whitespace,
/// If true, line selection will consider semantic prompt
/// state changing a boundary. State changing is ANY state
@@ -2652,10 +2653,10 @@ pub fn selectLine(self: *const Screen, opts: SelectLine) ?Selection {
if (!cell.hasText()) continue;
// Non-empty means we found it.
const this_whitespace = std.mem.indexOfAny(
const this_whitespace = std.mem.indexOfScalar(
u21,
whitespace,
&[_]u21{cell.content.codepoint},
cell.content.codepoint,
) != null;
if (this_whitespace) continue;
@@ -2674,10 +2675,10 @@ pub fn selectLine(self: *const Screen, opts: SelectLine) ?Selection {
if (!cell.hasText()) continue;
// Non-empty means we found it.
const this_whitespace = std.mem.indexOfAny(
const this_whitespace = std.mem.indexOfScalar(
u21,
whitespace,
&[_]u21{cell.content.codepoint},
cell.content.codepoint,
) != null;
if (this_whitespace) continue;
@@ -2798,10 +2799,10 @@ pub fn selectWord(
if (!start_cell.hasText()) return null;
// Determine if we are a boundary or not to determine what our boundary is.
const expect_boundary = std.mem.indexOfAny(
const expect_boundary = std.mem.indexOfScalar(
u21,
boundary_codepoints,
&[_]u21{start_cell.content.codepoint},
start_cell.content.codepoint,
) != null;
// Go forwards to find our end boundary
@@ -2816,10 +2817,10 @@ pub fn selectWord(
if (!cell.hasText()) break :end prev;
// If we do not match our expected set, we hit a boundary
const this_boundary = std.mem.indexOfAny(
const this_boundary = std.mem.indexOfScalar(
u21,
boundary_codepoints,
&[_]u21{cell.content.codepoint},
cell.content.codepoint,
) != null;
if (this_boundary != expect_boundary) break :end prev;
@@ -2853,10 +2854,10 @@ pub fn selectWord(
if (!cell.hasText()) break :start prev;
// If we do not match our expected set, we hit a boundary
const this_boundary = std.mem.indexOfAny(
const this_boundary = std.mem.indexOfScalar(
u21,
boundary_codepoints,
&[_]u21{cell.content.codepoint},
cell.content.codepoint,
) != null;
if (this_boundary != expect_boundary) break :start prev;

View File

@@ -170,6 +170,10 @@ pub const terminal_mode_get = terminal.mode_get;
pub const terminal_mode_set = terminal.mode_set;
pub const terminal_get = terminal.get;
pub const terminal_get_multi = terminal.get_multi;
pub const terminal_select_word = selection.word;
pub const terminal_select_line = selection.line;
pub const terminal_select_all = selection.all;
pub const terminal_select_output = selection.output;
pub const terminal_selection_adjust = selection.adjust;
pub const terminal_selection_order = selection.order;
pub const terminal_selection_ordered = selection.ordered;

View File

@@ -3,6 +3,7 @@ const testing = std.testing;
const lib = @import("../lib.zig");
const grid_ref = @import("grid_ref.zig");
const point = @import("../point.zig");
const selection_codepoints = @import("../selection_codepoints.zig");
const Selection = @import("../Selection.zig");
const Result = @import("result.zig").Result;
const terminal_c = @import("terminal.zig");
@@ -34,6 +35,130 @@ pub const CSelection = extern struct {
}
};
/// C: GhosttyTerminalSelectWordOptions
pub const SelectWordOptions = extern struct {
size: usize = @sizeOf(SelectWordOptions),
ref: grid_ref.CGridRef,
boundary_codepoints: ?[*]const u32 = null,
boundary_codepoints_len: usize = 0,
};
/// C: GhosttyTerminalSelectLineOptions
pub const SelectLineOptions = extern struct {
size: usize = @sizeOf(SelectLineOptions),
ref: grid_ref.CGridRef,
whitespace: ?[*]const u32 = null,
whitespace_len: usize = 0,
semantic_prompt_boundary: bool = false,
};
pub fn word(
terminal: terminal_c.Terminal,
options: ?*const SelectWordOptions,
out_selection: ?*CSelection,
) callconv(lib.calling_conv) Result {
const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
const opts = options orelse return .invalid_value;
if (opts.size < @sizeOf(SelectWordOptions)) return .invalid_value;
const out = out_selection orelse return .invalid_value;
const boundary_codepoints = codepointSlice(
opts.boundary_codepoints,
opts.boundary_codepoints_len,
) catch return .invalid_value;
const screen = t.screens.active;
const pin = opts.ref.toPin() orelse return .invalid_value;
out.* = .fromZig(screen.selectWord(
pin,
boundary_codepoints orelse &selection_codepoints.default_word_boundaries,
) orelse
return .no_value);
return .success;
}
pub fn line(
terminal: terminal_c.Terminal,
options: ?*const SelectLineOptions,
out_selection: ?*CSelection,
) callconv(lib.calling_conv) Result {
const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
const opts = options orelse return .invalid_value;
if (opts.size < @sizeOf(SelectLineOptions)) return .invalid_value;
const out = out_selection orelse return .invalid_value;
const whitespace = codepointSlice(
opts.whitespace,
opts.whitespace_len,
) catch return .invalid_value;
const screen = t.screens.active;
const pin = opts.ref.toPin() orelse return .invalid_value;
out.* = .fromZig(screen.selectLine(.{
.pin = pin,
.whitespace = whitespace orelse &selection_codepoints.default_line_whitespace,
.semantic_prompt_boundary = opts.semantic_prompt_boundary,
}) orelse return .no_value);
return .success;
}
pub fn all(
terminal: terminal_c.Terminal,
out_selection: ?*CSelection,
) callconv(lib.calling_conv) Result {
const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
const out = out_selection orelse return .invalid_value;
out.* = .fromZig(t.screens.active.selectAll() orelse return .no_value);
return .success;
}
pub fn output(
terminal: terminal_c.Terminal,
ref: grid_ref.CGridRef,
out_selection: ?*CSelection,
) callconv(lib.calling_conv) Result {
const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
const out = out_selection orelse return .invalid_value;
const screen = t.screens.active;
const pin = ref.toPin() orelse return .invalid_value;
out.* = .fromZig(screen.selectOutput(pin) orelse return .no_value);
return .success;
}
/// Return the borrowed C array of `uint32_t` codepoints as a `[]const u21`.
///
/// `NULL + len 0` returns null, which callers treat as “use the API default
/// set.” A non-null pointer with `len 0` returns an empty slice, meaning “use an
/// explicitly empty set.” A non-zero length requires a non-null pointer.
///
/// This is intentionally zero-copy. In the C ABI, codepoints are `uint32_t`,
/// but selection internals use Zig's `u21` to represent valid Unicode scalar
/// values. Zig currently stores `u21` in the same size and alignment as `u32`,
/// so we assert that layout relationship and reinterpret the borrowed slice.
/// If Zig ever changes that representation, these comptime assertions fail
/// loudly rather than silently making this cast wrong.
fn codepointSlice(
ptr: ?[*]const u32,
len: usize,
) error{InvalidValue}!?[]const u21 {
comptime {
std.debug.assert(@sizeOf(u21) == @sizeOf(u32));
std.debug.assert(@alignOf(u21) == @alignOf(u32));
}
if (len == 0) {
const p = ptr orelse return null;
_ = p;
return &.{};
}
const p = ptr orelse return error.InvalidValue;
const cps: [*]const u21 = @ptrCast(p);
return cps[0..len];
}
pub fn adjust(
terminal: terminal_c.Terminal,
selection: ?*CSelection,

View File

@@ -1393,6 +1393,72 @@ test "set and get selection" {
try testing.expectEqual(Result.no_value, get(t, .selection, @ptrCast(&out)));
}
test "selection derivation helpers" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib.alloc.test_allocator,
&t,
.{
.cols = 80,
.rows = 24,
.max_scrollback = 0,
},
));
defer free(t);
vt_write(t, " Hello \r\nWorld", 16);
var out: selection_c.CSelection = undefined;
var word_ref: grid_ref_c.CGridRef = .{};
try testing.expectEqual(Result.success, grid_ref(t, .{
.tag = .active,
.value = .{ .active = .{ .x = 3, .y = 0 } },
}, &word_ref));
var empty_ref: grid_ref_c.CGridRef = .{};
try testing.expectEqual(Result.success, grid_ref(t, .{
.tag = .active,
.value = .{ .active = .{ .x = 20, .y = 0 } },
}, &empty_ref));
var line_ref: grid_ref_c.CGridRef = .{};
try testing.expectEqual(Result.success, grid_ref(t, .{
.tag = .active,
.value = .{ .active = .{ .x = 0, .y = 0 } },
}, &line_ref));
var word_opts: selection_c.SelectWordOptions = .{
.ref = word_ref,
};
try testing.expectEqual(Result.success, selection_c.word(t, &word_opts, &out));
try testing.expectEqual(@as(u16, 2), out.start.toPin().?.x);
try testing.expectEqual(@as(u16, 6), out.end.toPin().?.x);
word_opts.ref = empty_ref;
try testing.expectEqual(Result.no_value, selection_c.word(t, &word_opts, &out));
var line_opts: selection_c.SelectLineOptions = .{
.ref = line_ref,
};
try testing.expectEqual(Result.success, selection_c.line(t, &line_opts, &out));
try testing.expectEqual(@as(u16, 2), out.start.toPin().?.x);
try testing.expectEqual(@as(u16, 6), out.end.toPin().?.x);
try testing.expectEqual(Result.success, selection_c.all(t, &out));
try testing.expectEqual(@as(u16, 2), out.start.toPin().?.x);
try testing.expectEqual(@as(u16, 0), out.start.toPin().?.y);
try testing.expectEqual(@as(u16, 4), out.end.toPin().?.x);
try testing.expectEqual(@as(u16, 1), out.end.toPin().?.y);
try testing.expectEqual(Result.no_value, selection_c.output(t, line_ref, &out));
line_opts.size = @sizeOf(usize) - 1;
try testing.expectEqual(Result.invalid_value, selection_c.line(t, &line_opts, &out));
try testing.expectEqual(Result.invalid_value, selection_c.word(t, null, &out));
try testing.expectEqual(Result.invalid_value, selection_c.word(t, &word_opts, null));
}
test "selection_adjust mutates snapshot end" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(

View File

@@ -20,30 +20,35 @@ const mouse_encode = @import("mouse_encode.zig");
const grid_ref = @import("grid_ref.zig");
/// All C API structs and their Ghostty C names.
pub const structs: std.StaticStringMap(StructInfo) = .initComptime(.{
.{ "GhosttyColorRgb", StructInfo.init(color.RGB.C) },
.{ "GhosttyDeviceAttributes", StructInfo.init(terminal.DeviceAttributes) },
.{ "GhosttyDeviceAttributesPrimary", StructInfo.init(terminal.DeviceAttributes.Primary) },
.{ "GhosttyDeviceAttributesSecondary", StructInfo.init(terminal.DeviceAttributes.Secondary) },
.{ "GhosttyDeviceAttributesTertiary", StructInfo.init(terminal.DeviceAttributes.Tertiary) },
.{ "GhosttyFormatterTerminalOptions", StructInfo.init(formatter.TerminalOptions) },
.{ "GhosttySelection", StructInfo.init(selection.CSelection) },
.{ "GhosttyFormatterTerminalExtra", StructInfo.init(formatter.TerminalOptions.Extra) },
.{ "GhosttyFormatterScreenExtra", StructInfo.init(formatter.ScreenOptions.Extra) },
.{ "GhosttyGridRef", StructInfo.init(grid_ref.CGridRef) },
.{ "GhosttyMouseEncoderSize", StructInfo.init(mouse_encode.Size) },
.{ "GhosttyMousePosition", StructInfo.init(mouse_event.Position) },
.{ "GhosttyPoint", StructInfo.init(point.Point.C) },
.{ "GhosttyPointCoordinate", StructInfo.init(point.Coordinate) },
.{ "GhosttyRenderStateColors", StructInfo.init(render.Colors) },
.{ "GhosttySizeReportSize", StructInfo.init(size_report.Size) },
.{ "GhosttyString", StructInfo.init(lib.String) },
.{ "GhosttyStyle", StructInfo.init(style_c.Style) },
.{ "GhosttyStyleColor", StructInfo.init(style_c.Color) },
.{ "GhosttyTerminalOptions", StructInfo.init(terminal.Options) },
.{ "GhosttyTerminalScrollbar", StructInfo.init(terminal.TerminalScrollbar) },
.{ "GhosttyTerminalScrollViewport", StructInfo.init(terminal.ScrollViewport) },
});
pub const structs: std.StaticStringMap(StructInfo) = structs: {
@setEvalBranchQuota(10_000);
break :structs .initComptime(.{
.{ "GhosttyColorRgb", StructInfo.init(color.RGB.C) },
.{ "GhosttyDeviceAttributes", StructInfo.init(terminal.DeviceAttributes) },
.{ "GhosttyDeviceAttributesPrimary", StructInfo.init(terminal.DeviceAttributes.Primary) },
.{ "GhosttyDeviceAttributesSecondary", StructInfo.init(terminal.DeviceAttributes.Secondary) },
.{ "GhosttyDeviceAttributesTertiary", StructInfo.init(terminal.DeviceAttributes.Tertiary) },
.{ "GhosttyFormatterTerminalOptions", StructInfo.init(formatter.TerminalOptions) },
.{ "GhosttySelection", StructInfo.init(selection.CSelection) },
.{ "GhosttyTerminalSelectWordOptions", StructInfo.init(selection.SelectWordOptions) },
.{ "GhosttyTerminalSelectLineOptions", StructInfo.init(selection.SelectLineOptions) },
.{ "GhosttyFormatterTerminalExtra", StructInfo.init(formatter.TerminalOptions.Extra) },
.{ "GhosttyFormatterScreenExtra", StructInfo.init(formatter.ScreenOptions.Extra) },
.{ "GhosttyGridRef", StructInfo.init(grid_ref.CGridRef) },
.{ "GhosttyMouseEncoderSize", StructInfo.init(mouse_encode.Size) },
.{ "GhosttyMousePosition", StructInfo.init(mouse_event.Position) },
.{ "GhosttyPoint", StructInfo.init(point.Point.C) },
.{ "GhosttyPointCoordinate", StructInfo.init(point.Coordinate) },
.{ "GhosttyRenderStateColors", StructInfo.init(render.Colors) },
.{ "GhosttySizeReportSize", StructInfo.init(size_report.Size) },
.{ "GhosttyString", StructInfo.init(lib.String) },
.{ "GhosttyStyle", StructInfo.init(style_c.Style) },
.{ "GhosttyStyleColor", StructInfo.init(style_c.Color) },
.{ "GhosttyTerminalOptions", StructInfo.init(terminal.Options) },
.{ "GhosttyTerminalScrollbar", StructInfo.init(terminal.TerminalScrollbar) },
.{ "GhosttyTerminalScrollViewport", StructInfo.init(terminal.ScrollViewport) },
});
};
/// The comptime-generated JSON string of all structs.
pub const json: [:0]const u8 = json: {

View File

@@ -0,0 +1,31 @@
// This file contains various default word boundaries used for
// selection logic. We put it in a separate file so that different
// subsystems can import it without introducing a number of
// dependencies.
/// Default boundary characters for word selection: ` \t'"│`|:;,()[]{}<>$`
pub const default_word_boundaries = [_]u21{
0, // null
' ', // space
'\t', // tab
'\'', // single quote
'"', // double quote
'│', // U+2502 box drawing
'`', // backtick
'|', // pipe
':', // colon
';', // semicolon
',', // comma
'(', // left paren
')', // right paren
'[', // left bracket
']', // right bracket
'{', // left brace
'}', // right brace
'<', // less than
'>', // greater than
'$', // dollar
};
/// Default whitespace characters trimmed from line selections.
pub const default_line_whitespace = [_]u21{ 0, ' ', '\t' };