mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
terminal: stringmaps
This commit is contained in:
@ -909,8 +909,18 @@ pub const Line = struct {
|
||||
start: usize,
|
||||
len: usize,
|
||||
|
||||
/// The string contents of this line.
|
||||
/// Return the string for this line.
|
||||
pub fn string(self: *const Line, alloc: Allocator) ![:0]const u8 {
|
||||
return try self.screen.selectionString(alloc, self.selection(), true);
|
||||
}
|
||||
|
||||
/// Receive the string for this line along with the byte-to-point mapping.
|
||||
pub fn stringMap(self: *const Line, alloc: Allocator) !StringMap {
|
||||
return try self.screen.selectionStringMap(alloc, self.selection());
|
||||
}
|
||||
|
||||
/// Return a selection that covers the entire line.
|
||||
pub fn selection(self: *const Line) Selection {
|
||||
// Get the start and end screen point.
|
||||
const start_idx = self.tag.index(self.start).toScreen(self.screen).screen;
|
||||
const end_idx = self.tag.index(self.start + (self.len - 1)).toScreen(self.screen).screen;
|
||||
@ -918,11 +928,10 @@ pub const Line = struct {
|
||||
// Convert the start and end screen points into a selection across
|
||||
// the entire rows. We then use selectionString because it handles
|
||||
// unwrapping, graphemes, etc.
|
||||
const sel: Selection = .{
|
||||
return .{
|
||||
.start = .{ .y = start_idx, .x = 0 },
|
||||
.end = .{ .y = end_idx, .x = self.screen.cols - 1 },
|
||||
};
|
||||
return try self.screen.selectionString(alloc, sel, false);
|
||||
}
|
||||
};
|
||||
|
||||
@ -2146,6 +2155,87 @@ pub fn selectionString(
|
||||
var strbuilder = try std.ArrayList(u8).initCapacity(alloc, slices.rows * self.cols);
|
||||
defer strbuilder.deinit();
|
||||
|
||||
// Get our string result.
|
||||
try self.selectionSliceString(slices, &strbuilder, null);
|
||||
|
||||
// Remove any trailing spaces on lines. We could do optimize this by
|
||||
// doing this in the loop above but this isn't very hot path code and
|
||||
// this is simple.
|
||||
if (trim) {
|
||||
var it = std.mem.tokenize(u8, strbuilder.items, "\n");
|
||||
|
||||
// Reset our items. We retain our capacity. Because we're only
|
||||
// removing bytes, we know that the trimmed string must be no longer
|
||||
// than the original string so we copy directly back into our
|
||||
// allocated memory.
|
||||
strbuilder.clearRetainingCapacity();
|
||||
while (it.next()) |line| {
|
||||
const trimmed = std.mem.trimRight(u8, line, " \t");
|
||||
const i = strbuilder.items.len;
|
||||
strbuilder.items.len += trimmed.len;
|
||||
std.mem.copyForwards(u8, strbuilder.items[i..], trimmed);
|
||||
strbuilder.appendAssumeCapacity('\n');
|
||||
}
|
||||
|
||||
// Remove our trailing newline again
|
||||
if (strbuilder.items.len > 0) strbuilder.items.len -= 1;
|
||||
}
|
||||
|
||||
// Get our final string
|
||||
const string = try strbuilder.toOwnedSliceSentinel(0);
|
||||
errdefer alloc.free(string);
|
||||
|
||||
return string;
|
||||
}
|
||||
|
||||
/// A string along with the mapping of each individual byte in the string
|
||||
/// to the point in the screen.
|
||||
pub const StringMap = struct {
|
||||
string: [:0]const u8,
|
||||
map: []point.ScreenPoint,
|
||||
|
||||
pub fn deinit(self: StringMap, alloc: Allocator) void {
|
||||
alloc.free(self.string);
|
||||
alloc.free(self.map);
|
||||
}
|
||||
};
|
||||
|
||||
/// Returns the row text associated with a selection along with the
|
||||
/// mapping of each individual byte in the string to the point in the screen.
|
||||
fn selectionStringMap(
|
||||
self: *Screen,
|
||||
alloc: Allocator,
|
||||
sel: Selection,
|
||||
) !StringMap {
|
||||
// Get the slices for the string
|
||||
const slices = self.selectionSlices(sel);
|
||||
|
||||
// Use an ArrayList so that we can grow the array as we go. We
|
||||
// build an initial capacity of just our rows in our selection times
|
||||
// columns. It can be more or less based on graphemes, newlines, etc.
|
||||
var strbuilder = try std.ArrayList(u8).initCapacity(alloc, slices.rows * self.cols);
|
||||
defer strbuilder.deinit();
|
||||
var mapbuilder = try std.ArrayList(point.ScreenPoint).initCapacity(alloc, strbuilder.capacity);
|
||||
defer mapbuilder.deinit();
|
||||
|
||||
// Get our results
|
||||
try self.selectionSliceString(slices, &strbuilder, &mapbuilder);
|
||||
|
||||
// Get our final string
|
||||
const string = try strbuilder.toOwnedSliceSentinel(0);
|
||||
errdefer alloc.free(string);
|
||||
const map = try mapbuilder.toOwnedSlice();
|
||||
errdefer alloc.free(map);
|
||||
return .{ .string = string, .map = map };
|
||||
}
|
||||
|
||||
/// Takes a SelectionSlices value and builds the string and mapping for it.
|
||||
fn selectionSliceString(
|
||||
self: *Screen,
|
||||
slices: SelectionSlices,
|
||||
strbuilder: *std.ArrayList(u8),
|
||||
mapbuilder: ?*std.ArrayList(point.ScreenPoint),
|
||||
) !void {
|
||||
// Connect the text from the two slices
|
||||
const arr = [_][]StorageCell{ slices.top, slices.bot };
|
||||
var row_count: usize = 0;
|
||||
@ -2194,17 +2284,37 @@ pub fn selectionString(
|
||||
{
|
||||
const encode_len = try std.unicode.utf8Encode(@intCast(char), &buf);
|
||||
try strbuilder.appendSlice(buf[0..encode_len]);
|
||||
if (mapbuilder) |b| {
|
||||
for (0..encode_len) |_| try b.append(.{
|
||||
.x = x,
|
||||
.y = slices.sel.start.y + row_i,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var cp_it = row.codepointIterator(x);
|
||||
while (cp_it.next()) |cp| {
|
||||
const encode_len = try std.unicode.utf8Encode(cp, &buf);
|
||||
try strbuilder.appendSlice(buf[0..encode_len]);
|
||||
if (mapbuilder) |b| {
|
||||
for (0..encode_len) |_| try b.append(.{
|
||||
.x = x,
|
||||
.y = slices.sel.start.y + row_i,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If this row is not soft-wrapped, add a newline
|
||||
if (!row.header().flags.wrap) try strbuilder.append('\n');
|
||||
if (!row.header().flags.wrap) {
|
||||
try strbuilder.append('\n');
|
||||
if (mapbuilder) |b| {
|
||||
try b.append(.{
|
||||
.x = self.cols - 1,
|
||||
.y = slices.sel.start.y + row_i,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2213,42 +2323,17 @@ pub fn selectionString(
|
||||
strbuilder.items[strbuilder.items.len - 1] == '\n')
|
||||
{
|
||||
strbuilder.items.len -= 1;
|
||||
if (mapbuilder) |b| b.items.len -= 1;
|
||||
}
|
||||
|
||||
// Remove any trailing spaces on lines. We could do optimize this by
|
||||
// doing this in the loop above but this isn't very hot path code and
|
||||
// this is simple.
|
||||
if (trim) {
|
||||
var it = std.mem.tokenize(u8, strbuilder.items, "\n");
|
||||
|
||||
// Reset our items. We retain our capacity. Because we're only
|
||||
// removing bytes, we know that the trimmed string must be no longer
|
||||
// than the original string so we copy directly back into our
|
||||
// allocated memory.
|
||||
strbuilder.clearRetainingCapacity();
|
||||
while (it.next()) |line| {
|
||||
const trimmed = std.mem.trimRight(u8, line, " \t");
|
||||
const i = strbuilder.items.len;
|
||||
strbuilder.items.len += trimmed.len;
|
||||
std.mem.copyForwards(u8, strbuilder.items[i..], trimmed);
|
||||
strbuilder.appendAssumeCapacity('\n');
|
||||
if (std.debug.runtime_safety) {
|
||||
if (mapbuilder) |b| {
|
||||
assert(strbuilder.items.len == b.items.len);
|
||||
}
|
||||
|
||||
// Remove our trailing newline again
|
||||
if (strbuilder.items.len > 0) strbuilder.items.len -= 1;
|
||||
}
|
||||
|
||||
// Get our final string
|
||||
const string = try strbuilder.toOwnedSliceSentinel(0);
|
||||
errdefer alloc.free(string);
|
||||
|
||||
return string;
|
||||
}
|
||||
|
||||
/// Returns the slices that make up the selection, in order. There are at most
|
||||
/// two parts to handle the ring buffer. If the selection fits in one contiguous
|
||||
/// slice, then the second slice will have a length of zero.
|
||||
fn selectionSlices(self: *Screen, sel_raw: Selection) struct {
|
||||
const SelectionSlices = struct {
|
||||
rows: usize,
|
||||
|
||||
// The selection that the slices below represent. This may not
|
||||
@ -2261,7 +2346,12 @@ fn selectionSlices(self: *Screen, sel_raw: Selection) struct {
|
||||
top_offset: usize,
|
||||
top: []StorageCell,
|
||||
bot: []StorageCell,
|
||||
} {
|
||||
};
|
||||
|
||||
/// Returns the slices that make up the selection, in order. There are at most
|
||||
/// two parts to handle the ring buffer. If the selection fits in one contiguous
|
||||
/// slice, then the second slice will have a length of zero.
|
||||
fn selectionSlices(self: *Screen, sel_raw: Selection) SelectionSlices {
|
||||
// Note: this function is tested via selectionString
|
||||
|
||||
// If the selection starts beyond the end of the screen, then we return empty
|
||||
|
Reference in New Issue
Block a user