horizontal tab, implement tabstops using a default value

This commit is contained in:
Mitchell Hashimoto
2022-04-27 20:21:16 -07:00
parent 3857e7f519
commit 15a816f863
3 changed files with 205 additions and 12 deletions

View File

@ -161,7 +161,7 @@ pub fn create(alloc: Allocator, loop: libuv.Loop) !*Window {
try stream.readStart(ttyReadAlloc, ttyRead);
// Create our terminal
var term = Terminal.init(grid.size.columns, grid.size.rows);
var term = try Terminal.init(alloc, grid.size.columns, grid.size.rows);
errdefer term.deinit(alloc);
// Setup a timer for blinking the cursor

179
src/terminal/Tabstops.zig Normal file
View File

@ -0,0 +1,179 @@
//! Keep track of the location of tabstops.
//!
//! This is implemented as a bit set. There is a preallocation segment that
//! is used for almost all screen sizes. Then there is a dynamically allocated
//! segment if the screen is larger than the preallocation amount.
//!
//! In reality, tabstops don't need to be the most performant in any metric.
//! This implementation tries to balance denser memory usage (by using a bitset)
//! and minimizing unnecessary allocations.
const Tabstops = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const testing = std.testing;
const assert = std.debug.assert;
/// Unit is the type we use per tabstop unit (see file docs).
const Unit = u8;
const unit_bits = @bitSizeOf(Unit);
/// The number of columsn we preallocate for. This is kind of high which
/// costs us some memory, but this is more columns than my 6k monitor at
/// 12-point font size, so this should prevent allocation in almost all
/// real world scenarios for the price of wasting at most
/// (columns / sizeOf(Unit)) bytes.
const prealloc_columns = 512;
/// The number of entries we need for our preallocation.
const prealloc_count = prealloc_columns / unit_bits;
/// We precompute all the possible masks since we never use a huge bit size.
const masks = blk: {
var res: [unit_bits]Unit = undefined;
for (res) |_, i| {
res[i] = @shlExact(@as(Unit, 1), @intCast(u3, i));
}
break :blk res;
};
/// Preallocated tab stops.
prealloc_stops: [prealloc_count]Unit = [1]Unit{0} ** prealloc_count,
/// Dynamically expanded stops above prealloc stops.
dynamic_stops: []Unit = &[0]Unit{},
/// Returns the entry in the stops array that would contain this column.
inline fn entry(col: usize) usize {
return col / unit_bits;
}
inline fn index(col: usize) usize {
return @mod(col, unit_bits);
}
pub fn init(alloc: Allocator, cols: usize, interval: usize) !Tabstops {
var res: Tabstops = .{};
try res.resize(alloc, cols);
res.reset(interval);
return res;
}
pub fn deinit(self: *Tabstops, alloc: Allocator) void {
if (self.dynamic_stops.len > 0) alloc.free(self.dynamic_stops);
self.* = undefined;
}
/// Set the tabstop at a certain column.
pub fn set(self: *Tabstops, col: usize) void {
const i = entry(col);
const idx = index(col);
if (i < prealloc_count) {
self.prealloc_stops[i] |= masks[idx];
return;
}
const dynamic_i = i - prealloc_count;
assert(dynamic_i < self.dynamic_stops.len);
self.dynamic_stops[dynamic_i] |= masks[idx];
}
/// Get the value of a tabstop at a specific column.
pub fn get(self: Tabstops, col: usize) bool {
const i = entry(col);
const idx = index(col);
const mask = masks[idx];
const unit = if (i < prealloc_count)
self.prealloc_stops[i]
else unit: {
const dynamic_i = i - prealloc_count;
assert(dynamic_i < self.dynamic_stops.len);
break :unit self.dynamic_stops[dynamic_i];
};
return unit & mask == mask;
}
/// Resize this to support up to cols columns.
pub fn resize(self: *Tabstops, alloc: Allocator, cols: usize) !void {
// Do nothing if it fits.
if (cols <= prealloc_columns) return;
// What we need in the dynamic size
const size = cols - prealloc_columns;
if (size < self.dynamic_stops.len) return;
// Note: we can probably try to realloc here but I'm not sure it matters.
const new = try alloc.alloc(Unit, size);
if (self.dynamic_stops.len > 0) {
std.mem.copy(Unit, new, self.dynamic_stops);
alloc.free(self.dynamic_stops);
}
self.dynamic_stops = new;
}
/// Return the total number of columns this can support currently.
pub fn capacity(self: Tabstops) usize {
return prealloc_count + self.dynamic_stops.len;
}
/// Unset all tabstops and then reset the initial tabstops to the given
/// interval. An interval of 0 sets no tabstops.
pub fn reset(self: *Tabstops, interval: usize) void {
std.mem.set(Unit, &self.prealloc_stops, 0);
std.mem.set(Unit, self.dynamic_stops, 0);
if (interval > 0) {
const cap = self.capacity();
var i: usize = interval - 1;
while (i < cap) : (i += interval) {
self.set(i);
}
}
}
test "Tabstops: basic" {
var t: Tabstops = .{};
defer t.deinit(testing.allocator);
try testing.expectEqual(@as(usize, 0), entry(4));
try testing.expectEqual(@as(usize, 1), entry(9));
try testing.expectEqual(@as(Unit, 0b00001000), masks[3]);
try testing.expectEqual(@as(Unit, 0b00010000), masks[4]);
try testing.expect(!t.get(4));
t.set(4);
try testing.expect(t.get(4));
try testing.expect(!t.get(3));
t.reset(0);
try testing.expect(!t.get(4));
}
test "Tabstops: dynamic allocations" {
var t: Tabstops = .{};
defer t.deinit(testing.allocator);
// Grow the capacity by 2.
const cap = t.capacity();
try t.resize(testing.allocator, cap * 2);
// Set something that was out of range of the first
t.set(cap + 5);
try testing.expect(t.get(cap + 5));
try testing.expect(!t.get(cap + 4));
// Prealloc still works
try testing.expect(!t.get(5));
}
test "Tabstops: interval" {
var t: Tabstops = try init(testing.allocator, 80, 4);
defer t.deinit(testing.allocator);
try testing.expect(!t.get(0));
try testing.expect(t.get(3));
try testing.expect(!t.get(4));
try testing.expect(t.get(7));
}

View File

@ -9,6 +9,7 @@ const testing = std.testing;
const Allocator = std.mem.Allocator;
const ansi = @import("ansi.zig");
const Parser = @import("Parser.zig");
const Tabstops = @import("Tabstops.zig");
const log = std.log.scoped(.terminal);
@ -18,6 +19,9 @@ screen: Screen,
/// Cursor position.
cursor: Cursor,
/// Where the tabstops are.
tabstops: Tabstops,
/// The size of the terminal.
rows: usize,
cols: usize,
@ -49,17 +53,19 @@ const Cursor = struct {
};
/// Initialize a new terminal.
pub fn init(cols: usize, rows: usize) Terminal {
return .{
pub fn init(alloc: Allocator, cols: usize, rows: usize) !Terminal {
return Terminal{
.cols = cols,
.rows = rows,
.screen = .{},
.cursor = .{ .x = 0, .y = 0 },
.tabstops = try Tabstops.init(alloc, cols, 8),
.parser = Parser.init(),
};
}
pub fn deinit(self: *Terminal, alloc: Allocator) void {
self.tabstops.deinit(alloc);
for (self.screen.items) |*line| line.deinit(alloc);
self.screen.deinit(alloc);
self.* = undefined;
@ -113,7 +119,7 @@ pub fn appendChar(self: *Terminal, alloc: Allocator, c: u8) !void {
for (actions) |action_opt| {
switch (action_opt orelse continue) {
.print => |p| try self.print(alloc, p),
.execute => |code| try self.execute(code),
.execute => |code| try self.execute(alloc, code),
}
}
}
@ -129,11 +135,11 @@ fn print(self: *Terminal, alloc: Allocator, c: u8) !void {
self.cursor.x += 1;
}
fn execute(self: *Terminal, c: u8) !void {
fn execute(self: *Terminal, alloc: Allocator, c: u8) !void {
switch (@intToEnum(ansi.C0, c)) {
.BEL => self.bell(),
.BS => self.backspace(),
.HT => self.horizontal_tab(),
.HT => try self.horizontal_tab(alloc),
.LF => self.linefeed(),
.CR => self.carriage_return(),
}
@ -150,9 +156,16 @@ pub fn backspace(self: *Terminal) void {
self.cursor.x -|= 1;
}
/// TODO
pub fn horizontal_tab(self: *Terminal) void {
_ = self;
/// Horizontal tab moves the cursor to the next tabstop, clearing
/// the screen to the left the tabstop.
pub fn horizontal_tab(self: *Terminal, alloc: Allocator) !void {
while (self.cursor.x < self.cols) {
// Clear
try self.print(alloc, ' ');
// If this is the tabstop, then we're done.
if (self.tabstops.get(self.cursor.x)) return;
}
}
/// Carriage return moves the cursor to the first column.
@ -184,10 +197,11 @@ fn getOrPutCell(self: *Terminal, alloc: Allocator, x: usize, y: usize) !*Cell {
test {
_ = Parser;
_ = Tabstops;
}
test "Terminal: input with no control characters" {
var t = init(80, 80);
var t = try init(testing.allocator, 80, 80);
defer t.deinit(testing.allocator);
// Basic grid writing
@ -202,7 +216,7 @@ test "Terminal: input with no control characters" {
}
test "Terminal: C0 control LF and CR" {
var t = init(80, 80);
var t = try init(testing.allocator, 80, 80);
defer t.deinit(testing.allocator);
// Basic grid writing
@ -217,7 +231,7 @@ test "Terminal: C0 control LF and CR" {
}
test "Terminal: C0 control BS" {
var t = init(80, 80);
var t = try init(testing.allocator, 80, 80);
defer t.deinit(testing.allocator);
// BS