WIP: first working version
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 55s
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 55s
It still has a minor memory leak and has at least two hacks implemented that I would like to improve on.
This commit is contained in:
@@ -3,8 +3,8 @@
|
|||||||
.version = "0.0.1",
|
.version = "0.0.1",
|
||||||
.dependencies = .{
|
.dependencies = .{
|
||||||
.zterm = .{
|
.zterm = .{
|
||||||
.url = "git+https://gitea.yves-biener.de/yves-biener/zterm#855594a8c836723f0230bfd6ad24f47613a147b1",
|
.url = "git+https://gitea.yves-biener.de/yves-biener/zterm#e972a2ea0f7a9f8caffd439ef206474b46475f91",
|
||||||
.hash = "zterm-0.3.0-1xmmEM8eHAB0cA7KLXGC7C8Nt7YEJcyoTme4domF1Yty",
|
.hash = "zterm-0.3.0-1xmmENkhHAB2rmNJFH-9rRqiRLnT673xwuMrqLwOnlT_",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
.minimum_zig_version = "0.16.0-dev.1254+bf15c791f",
|
.minimum_zig_version = "0.16.0-dev.1254+bf15c791f",
|
||||||
|
|||||||
269
src/elements.zig
Normal file
269
src/elements.zig
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
//! Applications `Element` implementations to be used in *zterm* `Container`'s
|
||||||
|
|
||||||
|
pub fn Root(App: type) type {
|
||||||
|
return struct {
|
||||||
|
gpa: Allocator,
|
||||||
|
diffs: std.ArrayListUnmanaged(App.Scrollable) = .empty,
|
||||||
|
container: *App.Container = undefined,
|
||||||
|
|
||||||
|
pub fn init(gpa: Allocator) @This() {
|
||||||
|
return .{ .gpa = gpa };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deinit(ctx: *anyopaque) void {
|
||||||
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||||
|
for (this.diffs.items) |*scrollable| scrollable.element().deinit();
|
||||||
|
this.diffs.deinit(this.gpa);
|
||||||
|
// the container is the root container, which is calling this function, do not deinitialize it!
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn element(this: *@This()) App.Element {
|
||||||
|
return .{
|
||||||
|
.ptr = this,
|
||||||
|
.vtable = &.{
|
||||||
|
.deinit = deinit,
|
||||||
|
.handle = handle,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle(ctx: *anyopaque, _: *App.Model, event: App.Event) !void {
|
||||||
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||||
|
switch (event) {
|
||||||
|
// NOTE assume that there is at least one diff
|
||||||
|
.init => this.container.elements.items[this.container.elements.items.len - 1].element = this.diffs.items[0].element(),
|
||||||
|
.file => |idx| this.container.elements.items[this.container.elements.items.len - 1].element = this.diffs.items[idx].element(),
|
||||||
|
.quit => this.container.elements.items[this.container.elements.items.len - 1].element = .{},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE base contents from the model!
|
||||||
|
// -> assume that the model does not change! it is created initially and then never changed!
|
||||||
|
pub fn Tree(App: type) type {
|
||||||
|
return struct {
|
||||||
|
scrollback: usize = 0, // scrollback index for the changed files
|
||||||
|
idx: usize = 0, // index for the currently selected entry
|
||||||
|
len: usize = 0, // total amount of changed files
|
||||||
|
size: Point = .{}, // current available size for the `Element`
|
||||||
|
queue: *App.Queue,
|
||||||
|
|
||||||
|
pub fn init(queue: *App.Queue) @This() {
|
||||||
|
return .{ .queue = queue };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn element(this: *@This()) App.Element {
|
||||||
|
return .{
|
||||||
|
.ptr = this,
|
||||||
|
.vtable = &.{
|
||||||
|
.resize = resize,
|
||||||
|
.handle = handle,
|
||||||
|
.content = content,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resize(ctx: *anyopaque, _: *const App.Model, size: Point) void {
|
||||||
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||||
|
this.size = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle(ctx: *anyopaque, model: *App.Model, event: App.Event) !void {
|
||||||
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||||
|
switch (event) {
|
||||||
|
.init => {
|
||||||
|
this.len = model.changes.keys().len;
|
||||||
|
this.scrollback = 0;
|
||||||
|
this.idx = 0;
|
||||||
|
},
|
||||||
|
// TODO also support key inputs to change the current file?
|
||||||
|
.mouse => |mouse| if (this.len > 0) switch (mouse.button) {
|
||||||
|
.left => if (mouse.y + this.scrollback < this.len) {
|
||||||
|
this.idx = mouse.y + this.scrollback;
|
||||||
|
this.queue.push(.{ .file = this.idx });
|
||||||
|
},
|
||||||
|
.wheel_up => this.scrollback -|= 3,
|
||||||
|
.wheel_down => this.scrollback = @min(this.len - 1, this.scrollback + 3),
|
||||||
|
else => {},
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn content(ctx: *anyopaque, model: *const App.Model, cells: []Cell, size: Point) !void {
|
||||||
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||||
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||||
|
if (this.len == 0) return;
|
||||||
|
|
||||||
|
var cell_idx: usize = 0;
|
||||||
|
const changes: []const Model.Index = model.changes.keys();
|
||||||
|
const files = changes[this.scrollback..];
|
||||||
|
|
||||||
|
for (0..size.y) |row| {
|
||||||
|
if (row >= files.len) break;
|
||||||
|
const idx = this.scrollback + row;
|
||||||
|
|
||||||
|
const file = files[row];
|
||||||
|
const value = model.content[file.idx .. file.idx + file.len];
|
||||||
|
var value_idx: usize = 0;
|
||||||
|
if (value_idx >= value.len) break;
|
||||||
|
const row_color: zterm.Color = if (this.idx == idx) .blue else .default;
|
||||||
|
cell_idx = row * size.x;
|
||||||
|
for (0..size.x) |_| {
|
||||||
|
cell_idx += 1;
|
||||||
|
if (value_idx >= value.len) break;
|
||||||
|
const cp = value[value_idx];
|
||||||
|
defer value_idx += 1;
|
||||||
|
|
||||||
|
if (cell_idx >= cells.len) break;
|
||||||
|
switch (cp) {
|
||||||
|
'\n' => cell_idx += 1,
|
||||||
|
'\t' => cell_idx += 2,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
cells[cell_idx].cp = switch (cp) {
|
||||||
|
'\n', '\t' => continue,
|
||||||
|
else => cp,
|
||||||
|
};
|
||||||
|
cells[cell_idx].style.fg = row_color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Individual `Change` associated to a parsed diff header.
|
||||||
|
pub fn Change(App: type) type {
|
||||||
|
return struct {
|
||||||
|
change: Model.Index,
|
||||||
|
// how does an individual `Change` knows it relation to the `Model` containing it's contents?
|
||||||
|
pub fn init(change: Model.Index) @This() {
|
||||||
|
return .{ .change = change };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn element(this: *@This()) App.Element {
|
||||||
|
return .{
|
||||||
|
.ptr = this,
|
||||||
|
.vtable = &.{
|
||||||
|
.minSize = minSize,
|
||||||
|
.content = content,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn minSize(ctx: *anyopaque, model: *const App.Model, size: Point) Point {
|
||||||
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||||
|
// TODO handle `\n`, '\t', etc. correctly in the fill for the `cells`
|
||||||
|
switch (model.render_mode) {
|
||||||
|
.side_by_side => {},
|
||||||
|
.stacked => {},
|
||||||
|
}
|
||||||
|
var y: u16 = 0;
|
||||||
|
const value = model.content[this.change.idx .. this.change.idx + this.change.len];
|
||||||
|
if (this.change.idx + this.change.len == model.content.len) y += 1;
|
||||||
|
for (value) |cp| if (cp == '\n') {
|
||||||
|
y += 1;
|
||||||
|
};
|
||||||
|
return .{ .x = size.x, .y = y };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn content(ctx: *anyopaque, model: *const App.Model, cells: []Cell, size: Point) !void {
|
||||||
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||||
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||||
|
// TODO render different modes accordingly
|
||||||
|
var cell_idx: usize = 0;
|
||||||
|
const value = model.content[this.change.idx .. this.change.idx + this.change.len];
|
||||||
|
var value_idx: usize = 0;
|
||||||
|
switch (model.render_mode) {
|
||||||
|
.side_by_side => for (0..size.y) |row| {
|
||||||
|
if (value_idx >= value.len) break;
|
||||||
|
const row_color: zterm.Color = switch (value[value_idx]) {
|
||||||
|
'+' => .green,
|
||||||
|
'-' => .red,
|
||||||
|
'@' => .blue,
|
||||||
|
else => .default,
|
||||||
|
};
|
||||||
|
|
||||||
|
cell_idx = row * size.x;
|
||||||
|
for (0..size.x) |_| {
|
||||||
|
cell_idx += 1;
|
||||||
|
if (value_idx >= value.len) break;
|
||||||
|
const cp = value[value_idx];
|
||||||
|
defer value_idx += 1;
|
||||||
|
|
||||||
|
if (cell_idx >= cells.len) break;
|
||||||
|
switch (cp) {
|
||||||
|
'\n' => {},
|
||||||
|
'\t' => cell_idx += 2,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
cells[cell_idx].cp = switch (cp) {
|
||||||
|
'\n' => break,
|
||||||
|
'\t' => continue,
|
||||||
|
else => cp,
|
||||||
|
};
|
||||||
|
cells[cell_idx].style.fg = row_color;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.stacked => for (0..size.y) |row| {
|
||||||
|
if (value_idx >= value.len) break;
|
||||||
|
const row_color: zterm.Color = switch (value[value_idx]) {
|
||||||
|
'+' => .green,
|
||||||
|
'-' => .red,
|
||||||
|
'@' => .blue,
|
||||||
|
else => .default,
|
||||||
|
};
|
||||||
|
|
||||||
|
cell_idx = row * size.x;
|
||||||
|
for (0..size.x) |_| {
|
||||||
|
cell_idx += 1;
|
||||||
|
if (value_idx >= value.len) break;
|
||||||
|
const cp = value[value_idx];
|
||||||
|
defer value_idx += 1;
|
||||||
|
|
||||||
|
if (cell_idx >= cells.len) break;
|
||||||
|
switch (cp) {
|
||||||
|
'\n' => {},
|
||||||
|
'\t' => cell_idx += 2,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
cells[cell_idx].cp = switch (cp) {
|
||||||
|
'\n' => break,
|
||||||
|
'\t' => continue,
|
||||||
|
else => cp,
|
||||||
|
};
|
||||||
|
cells[cell_idx].style.fg = row_color;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Diff` describes a collection of `Change`'s (i.e. through a root `Container` inside an `App.Scrollable`)
|
||||||
|
pub fn Diff(App: type, gpa: Allocator, changes: []Change(App)) !App.Scrollable {
|
||||||
|
var diff: App.Container = try .init(gpa, .{
|
||||||
|
.layout = .{
|
||||||
|
.direction = .vertical,
|
||||||
|
.separator = .{
|
||||||
|
.enabled = true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, .{});
|
||||||
|
for (changes) |*change| try diff.append(try .init(gpa, .{}, change.element()));
|
||||||
|
return .init(diff, .disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
var random_alg = std.Random.DefaultCsprng.init(@splat(1));
|
||||||
|
var random = random_alg.random();
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const zterm = @import("zterm");
|
||||||
|
const Model = @import("model.zig");
|
||||||
|
|
||||||
|
const Cell = zterm.Cell;
|
||||||
|
const Point = zterm.Point;
|
||||||
395
src/lexer.zig
Normal file
395
src/lexer.zig
Normal file
@@ -0,0 +1,395 @@
|
|||||||
|
///! Lexer for *unified diff* format to tokenize input sources accordningly.
|
||||||
|
pub const Token = struct {
|
||||||
|
tag: Tag,
|
||||||
|
loc: Location,
|
||||||
|
|
||||||
|
pub const Location = struct {
|
||||||
|
idx: usize,
|
||||||
|
len: usize,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Tag = enum(u8) {
|
||||||
|
/// File information; contains the content of:
|
||||||
|
/// ```
|
||||||
|
/// --- a/xxx
|
||||||
|
/// --- b/xxx
|
||||||
|
/// ```
|
||||||
|
/// *NOTE* includes trailing newline character
|
||||||
|
file,
|
||||||
|
/// Hunk header information; contains content of:
|
||||||
|
/// ```@@ -x,y +z,y @@```
|
||||||
|
header,
|
||||||
|
/// may be diff content or filler content of the tools output
|
||||||
|
content,
|
||||||
|
/// invalid contents that could not be parsed correctly
|
||||||
|
invalid,
|
||||||
|
/// End of file
|
||||||
|
eof,
|
||||||
|
|
||||||
|
pub fn lexeme(tag: Tag) ?[]const u8 {
|
||||||
|
return switch (tag) {
|
||||||
|
.header => "@@ -x,y +z,y @@",
|
||||||
|
.content => "..",
|
||||||
|
.file => "diff --git a/xxx b/xxx",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn symbol(tag: Tag) []const u8 {
|
||||||
|
return tag.lexeme() orelse switch (tag) {
|
||||||
|
.eof => "EOF",
|
||||||
|
.invalid => "invalid",
|
||||||
|
else => unreachable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Tokenizer = struct {
|
||||||
|
buffer: [:0]const u8,
|
||||||
|
index: usize,
|
||||||
|
|
||||||
|
/// For debugging purposes
|
||||||
|
pub fn dump(self: *const Tokenizer, token: *const Token) void {
|
||||||
|
print(".{s} \"{s}\"\n", .{ @tagName(token.tag), self.buffer[token.loc.idx .. token.loc.idx + token.loc.len] });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(buffer: [:0]const u8) Tokenizer {
|
||||||
|
return .{
|
||||||
|
.buffer = buffer,
|
||||||
|
// skip the UTF-8 BOM if present
|
||||||
|
.index = if (mem.startsWith(u8, buffer, "\xEF\xBB\xBF")) 3 else 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const State = enum {
|
||||||
|
default,
|
||||||
|
invalid,
|
||||||
|
at_sign,
|
||||||
|
minus,
|
||||||
|
header,
|
||||||
|
plus,
|
||||||
|
file,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// state fsm (finite state machine) describing the syntax of `nf`
|
||||||
|
/// TODO I need to draw one for all the possible states for tokenization!
|
||||||
|
/// -> for that I can create test cases!
|
||||||
|
/// -> detect valid and invalid syntax uses! this is however the job of the parser?
|
||||||
|
///
|
||||||
|
/// TODO points to improve on:
|
||||||
|
/// -> reduce duplicated code sections
|
||||||
|
/// -> make tags more explicit (i.e. remove unnecessary newlines, whitespaces, etc.)
|
||||||
|
/// -> streamline catching the common cases for tokens
|
||||||
|
/// -> reduce state machine
|
||||||
|
/// -> do not group tokens, instead this should be done by the parser when deriving the ast from the token stream
|
||||||
|
/// then the parser can identify missing parts and even point to the corresponding location in the file!
|
||||||
|
pub fn next(this: *Tokenizer) Token {
|
||||||
|
const token = this.next_token();
|
||||||
|
this.index = token.loc.idx + token.loc.len;
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_token(this: *const Tokenizer) Token {
|
||||||
|
var index = this.index;
|
||||||
|
var result: Token = .{
|
||||||
|
.tag = undefined,
|
||||||
|
.loc = .{
|
||||||
|
.idx = this.index,
|
||||||
|
.len = undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
state: switch (State.default) {
|
||||||
|
.default => switch (this.buffer[index]) {
|
||||||
|
0 => if (index == this.buffer.len) {
|
||||||
|
if (result.loc.idx != index) {
|
||||||
|
result.tag = .content;
|
||||||
|
} else {
|
||||||
|
return .{
|
||||||
|
.tag = .eof,
|
||||||
|
.loc = .{
|
||||||
|
.idx = index,
|
||||||
|
.len = 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
continue :state .invalid;
|
||||||
|
},
|
||||||
|
'@' => continue :state .at_sign,
|
||||||
|
'-' => continue :state .minus,
|
||||||
|
else => {
|
||||||
|
index += 1;
|
||||||
|
continue :state .default;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
.invalid => {
|
||||||
|
switch (this.buffer[index]) {
|
||||||
|
0 => result.tag = .invalid,
|
||||||
|
else => {
|
||||||
|
index += 1;
|
||||||
|
result.tag = .invalid;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.at_sign => {
|
||||||
|
index += 1;
|
||||||
|
switch (this.buffer[index]) {
|
||||||
|
'@' => if (result.loc.idx != index - 1) {
|
||||||
|
index -= 1;
|
||||||
|
result.tag = .content;
|
||||||
|
} else continue :state .header,
|
||||||
|
else => continue :state .default,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.header => {
|
||||||
|
index += 1;
|
||||||
|
switch (this.buffer[index]) {
|
||||||
|
'@' => if (this.buffer[index + 1] == '@') {
|
||||||
|
result.tag = .header;
|
||||||
|
index += 2;
|
||||||
|
} else continue :state .invalid,
|
||||||
|
0 => continue :state .invalid,
|
||||||
|
else => continue :state .header,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.minus => {
|
||||||
|
index += 1;
|
||||||
|
switch (this.buffer[index]) {
|
||||||
|
// assuming that we start with a minus!
|
||||||
|
'-' => if (this.buffer[index + 1] == '-') {
|
||||||
|
if (result.loc.idx != index - 1) {
|
||||||
|
index -= 1;
|
||||||
|
result.tag = .content;
|
||||||
|
} else {
|
||||||
|
index += 1;
|
||||||
|
continue :state .file;
|
||||||
|
}
|
||||||
|
} else continue :state .default,
|
||||||
|
0 => continue :state .invalid,
|
||||||
|
else => continue :state .default,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.file => {
|
||||||
|
// std.log.err(".file: {s}", .{this.buffer[index - 2 .. @min(index + 3, this.buffer.len)]});
|
||||||
|
index += 1;
|
||||||
|
switch (this.buffer[index]) {
|
||||||
|
'+' => if (this.buffer[index + 1] == '+' and this.buffer[index + 2] == '+') {
|
||||||
|
index += 2;
|
||||||
|
continue :state .plus;
|
||||||
|
} else continue :state .file,
|
||||||
|
0 => continue :state .invalid,
|
||||||
|
else => continue :state .file,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.plus => {
|
||||||
|
// std.log.err(".plus", .{});
|
||||||
|
index += 1;
|
||||||
|
switch (this.buffer[index]) {
|
||||||
|
'\n' => {
|
||||||
|
index += 1; // include newline
|
||||||
|
result.tag = .file;
|
||||||
|
},
|
||||||
|
0 => continue :state .invalid,
|
||||||
|
else => continue :state .plus,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result.loc.len = index - result.loc.idx;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const mem = std.mem;
|
||||||
|
const debug = std.debug;
|
||||||
|
const testing = std.testing;
|
||||||
|
const assert = debug.assert;
|
||||||
|
const print = debug.print;
|
||||||
|
|
||||||
|
test "individual change" {
|
||||||
|
try testTokenize(
|
||||||
|
\\diff --git a/build.zig.zon b/build.zig.zon
|
||||||
|
\\index 99bede4..a039487 100644
|
||||||
|
\\--- a/build.zig.zon
|
||||||
|
\\+++ b/build.zig.zon
|
||||||
|
\\@@ -3,8 +3,8 @@
|
||||||
|
\\ .version = "0.0.1",
|
||||||
|
\\ .dependencies = .{
|
||||||
|
\\ .zterm = .{
|
||||||
|
\\- .url = "git+https://gitea.yves-biener.de/yves-biener/zterm#855594a8c836723f0230bfd6ad24f47613a147b1",
|
||||||
|
\\- .hash = "zterm-0.3.0-1xmmEM8eHAB0cA7KLXGC7C8Nt7YEJcyoTme4domF1Yty",
|
||||||
|
\\+ .url = "git+https://gitea.yves-biener.de/yves-biener/zterm#e972a2ea0f7a9f8caffd439ef206474b46475f91",
|
||||||
|
\\+ .hash = "zterm-0.3.0-1xmmENkhHAB2rmNJFH-9rRqiRLnT673xwuMrqLwOnlT_",
|
||||||
|
\\ },
|
||||||
|
\\ },
|
||||||
|
\\ .minimum_zig_version = "0.16.0-dev.1254+bf15c791f",
|
||||||
|
, &.{ .content, .file, .header, .content, .eof });
|
||||||
|
}
|
||||||
|
|
||||||
|
test "individual changes in the different files" {
|
||||||
|
try testTokenize(
|
||||||
|
\\diff --git a/build.zig.zon b/build.zig.zon
|
||||||
|
\\index 99bede4..a039487 100644
|
||||||
|
\\--- a/build.zig.zon
|
||||||
|
\\+++ b/build.zig.zon
|
||||||
|
\\@@ -3,8 +3,8 @@
|
||||||
|
\\ .version = "0.0.1",
|
||||||
|
\\ .dependencies = .{
|
||||||
|
\\ .zterm = .{
|
||||||
|
\\- .url = "git+https://gitea.yves-biener.de/yves-biener/zterm#855594a8c836723f0230bfd6ad24f47613a147b1",
|
||||||
|
\\- .hash = "zterm-0.3.0-1xmmEM8eHAB0cA7KLXGC7C8Nt7YEJcyoTme4domF1Yty",
|
||||||
|
\\+ .url = "git+https://gitea.yves-biener.de/yves-biener/zterm#e972a2ea0f7a9f8caffd439ef206474b46475f91",
|
||||||
|
\\+ .hash = "zterm-0.3.0-1xmmENkhHAB2rmNJFH-9rRqiRLnT673xwuMrqLwOnlT_",
|
||||||
|
\\ },
|
||||||
|
\\ },
|
||||||
|
\\ .minimum_zig_version = "0.16.0-dev.1254+bf15c791f",
|
||||||
|
\\diff --git a/src/model.zig b/src/model.zig
|
||||||
|
\\index b402c51..defd874 100644
|
||||||
|
\\--- a/src/model.zig
|
||||||
|
\\+++ b/src/model.zig
|
||||||
|
\\@@ -30,3 +30,9 @@ pub const Change = struct {
|
||||||
|
\\
|
||||||
|
\\ const Model = @This();
|
||||||
|
\\ const std = @import("std");
|
||||||
|
\\+const lexer = @import("lexer.zig");
|
||||||
|
\\+
|
||||||
|
\\+test {
|
||||||
|
\\+ std.testing.refAllDeclsRecursive(@This());
|
||||||
|
\\+ _ = @import("lexer.zig");
|
||||||
|
\\+}
|
||||||
|
, &.{ .content, .file, .header, .content, .file, .header, .content, .eof });
|
||||||
|
}
|
||||||
|
|
||||||
|
test "multiple changes in same file" {
|
||||||
|
try testTokenize(
|
||||||
|
\\diff --git a/src/queue.zig b/src/queue.zig
|
||||||
|
\\index aae7ddf..2591b0a 100644
|
||||||
|
\\--- a/src/queue.zig
|
||||||
|
\\+++ b/src/queue.zig
|
||||||
|
\\@@ -215,7 +215,7 @@ fn sleepyPop(q: *Queue(u8, 2)) !void {
|
||||||
|
\\ // still full and the push in the other thread is still blocked
|
||||||
|
\\ // waiting for space.
|
||||||
|
\\ try Thread.yield();
|
||||||
|
\\- std.Thread.sleep(std.time.ns_per_s);
|
||||||
|
\\+ // std.Thread.sleep(std.time.ns_per_s);
|
||||||
|
\\ // Finally, let that other thread go.
|
||||||
|
\\ try testing.expectEqual(1, q.pop());
|
||||||
|
\\
|
||||||
|
\\@@ -225,7 +225,7 @@ fn sleepyPop(q: *Queue(u8, 2)) !void {
|
||||||
|
\\ try Thread.yield();
|
||||||
|
\\ // But we want to ensure that there's a second push waiting, so
|
||||||
|
\\ // here's another sleep.
|
||||||
|
\\- std.Thread.sleep(std.time.ns_per_s / 2);
|
||||||
|
\\+ // std.Thread.sleep(std.time.ns_per_s / 2);
|
||||||
|
\\
|
||||||
|
\\ // Another spurious wake...
|
||||||
|
\\ q.not_full.signal();
|
||||||
|
\\@@ -233,7 +233,7 @@ fn sleepyPop(q: *Queue(u8, 2)) !void {
|
||||||
|
\\ // And another chance for the other thread to see that it's
|
||||||
|
\\ // spurious and go back to sleep.
|
||||||
|
\\ try Thread.yield();
|
||||||
|
\\- std.Thread.sleep(std.time.ns_per_s / 2);
|
||||||
|
\\+ // std.Thread.sleep(std.time.ns_per_s / 2);
|
||||||
|
\\
|
||||||
|
\\ // Pop that thing and we're done.
|
||||||
|
\\ try testing.expectEqual(2, q.pop());
|
||||||
|
\\@@ -250,13 +250,13 @@ test "Fill, block, fill, block" {
|
||||||
|
\\ const thread = try Thread.spawn(cfg, sleepyPop, .{&queue});
|
||||||
|
\\ queue.push(1);
|
||||||
|
\\ queue.push(2);
|
||||||
|
\\- const now = std.time.milliTimestamp();
|
||||||
|
\\+ // const now = std.time.milliTimestamp();
|
||||||
|
\\ queue.push(3); // This one should block.
|
||||||
|
\\- const then = std.time.milliTimestamp();
|
||||||
|
\\+ // const then = std.time.milliTimestamp();
|
||||||
|
\\
|
||||||
|
\\ // Just to make sure the sleeps are yielding to this thread, make
|
||||||
|
\\ // sure it took at least 900ms to do the push.
|
||||||
|
\\- try testing.expect(then - now > 900);
|
||||||
|
\\+ // try testing.expect(then - now > 900);
|
||||||
|
\\
|
||||||
|
\\ // This should block again, waiting for the other thread.
|
||||||
|
\\ queue.push(4);
|
||||||
|
\\@@ -270,14 +270,14 @@ test "Fill, block, fill, block" {
|
||||||
|
\\ fn sleepyPush(q: *Queue(u8, 1)) !void {
|
||||||
|
\\ // Try to ensure the other thread has already started trying to pop.
|
||||||
|
\\ try Thread.yield();
|
||||||
|
\\- std.Thread.sleep(std.time.ns_per_s / 2);
|
||||||
|
\\+ // std.Thread.sleep(std.time.ns_per_s / 2);
|
||||||
|
\\
|
||||||
|
\\ // Spurious wake
|
||||||
|
\\ q.not_full.signal();
|
||||||
|
\\ q.not_empty.signal();
|
||||||
|
\\
|
||||||
|
\\ try Thread.yield();
|
||||||
|
\\- std.Thread.sleep(std.time.ns_per_s / 2);
|
||||||
|
\\+ // std.Thread.sleep(std.time.ns_per_s / 2);
|
||||||
|
\\
|
||||||
|
\\ // Stick something in the queue so it can be popped.
|
||||||
|
\\ q.push(1);
|
||||||
|
\\@@ -286,7 +286,7 @@ fn sleepyPush(q: *Queue(u8, 1)) !void {
|
||||||
|
\\ try Thread.yield();
|
||||||
|
\\ // Give the other thread time to block again.
|
||||||
|
\\ try Thread.yield();
|
||||||
|
\\- std.Thread.sleep(std.time.ns_per_s / 2);
|
||||||
|
\\+ // std.Thread.sleep(std.time.ns_per_s / 2);
|
||||||
|
\\
|
||||||
|
\\ // Spurious wake
|
||||||
|
\\ q.not_full.signal();
|
||||||
|
\\@@ -317,7 +317,7 @@ test "2 readers" {
|
||||||
|
\\ const t1 = try Thread.spawn(cfg, readerThread, .{&queue});
|
||||||
|
\\ const t2 = try Thread.spawn(cfg, readerThread, .{&queue});
|
||||||
|
\\ try Thread.yield();
|
||||||
|
\\- std.Thread.sleep(std.time.ns_per_s / 2);
|
||||||
|
\\+ // std.Thread.sleep(std.time.ns_per_s / 2);
|
||||||
|
\\ queue.push(1);
|
||||||
|
\\ queue.push(1);
|
||||||
|
\\ t1.join();
|
||||||
|
\\ );
|
||||||
|
, &.{
|
||||||
|
.content,
|
||||||
|
.file,
|
||||||
|
.header,
|
||||||
|
.content,
|
||||||
|
.header,
|
||||||
|
.content,
|
||||||
|
.header,
|
||||||
|
.content,
|
||||||
|
.header,
|
||||||
|
.content,
|
||||||
|
.header,
|
||||||
|
.content,
|
||||||
|
.header,
|
||||||
|
.content,
|
||||||
|
.header,
|
||||||
|
.content,
|
||||||
|
.eof,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test tokenizer's iterator outputs for the provided source. It should
|
||||||
|
/// match the expected token tags, except the very last .eof tag which shall
|
||||||
|
/// be omitted from the argument of expected_token_tags, as this function
|
||||||
|
/// explicitly tests for the .eof tag (with corresponding location information).
|
||||||
|
fn testTokenize(source: [:0]const u8, expected_token_tags: []const Token.Tag) !void {
|
||||||
|
var tokenizer = Tokenizer.init(source);
|
||||||
|
for (0.., expected_token_tags) |i, expected| {
|
||||||
|
const token = tokenizer.next();
|
||||||
|
testing.expectEqual(expected, token.tag) catch |err| {
|
||||||
|
print("Got token: ", .{});
|
||||||
|
tokenizer.dump(&token);
|
||||||
|
print("Expected .{s} at index {d}\n", .{ @tagName(expected), i });
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const last_token = tokenizer.next();
|
||||||
|
testing.expectEqual(Token.Tag.eof, last_token.tag) catch |err| {
|
||||||
|
print("Got token: ", .{});
|
||||||
|
tokenizer.dump(&last_token);
|
||||||
|
print("Expected .{s}\n", .{@tagName(Token.Tag.eof)});
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
try testing.expectEqual(source.len, last_token.loc.idx);
|
||||||
|
try testing.expectEqual(0, last_token.loc.len);
|
||||||
|
}
|
||||||
26
src/main.zig
26
src/main.zig
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
// FIX known issues:
|
// FIX known issues:
|
||||||
|
|
||||||
|
const diff = "";
|
||||||
|
|
||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
// argument handling
|
// argument handling
|
||||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||||
@@ -38,21 +40,27 @@ pub fn main() !void {
|
|||||||
var renderer = zterm.Renderer.Buffered.init(allocator);
|
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||||
defer renderer.deinit();
|
defer renderer.deinit();
|
||||||
|
|
||||||
var app: App = .init(io, .init);
|
var app: App = .init(io, try .init(allocator, diff));
|
||||||
|
defer app.model.deinit(allocator);
|
||||||
|
|
||||||
var element_root: tui_diff.elements.Root(App) = .init;
|
var element_root: tui_diff.elements.Root(App) = .init(allocator);
|
||||||
var element_tree: tui_diff.elements.Tree(App) = .init;
|
var element_tree: tui_diff.elements.Tree(App) = .init(&app.queue);
|
||||||
var changes: [3]tui_diff.elements.Change(App) = @splat(.init);
|
|
||||||
var scrollable_diffs = try tui_diff.elements.Diff(App, allocator, &changes);
|
|
||||||
// NOTE scrollable should provide deinit function (*zterm*)
|
|
||||||
// -> `Container` of `Scrollable` does not pass through the `minSize` request to its children!
|
|
||||||
|
|
||||||
var root = try tui_diff.Container(App, allocator, &element_root, &element_tree, &scrollable_diffs);
|
var iter = app.model.changes.iterator();
|
||||||
|
var entry = iter.next();
|
||||||
|
while (entry != null) : (entry = iter.next()) if (entry) |e| {
|
||||||
|
var changes = try allocator.alloc(tui_diff.elements.Change(App), e.value_ptr.items.len);
|
||||||
|
for (0.., e.value_ptr.items) |i, diff_index|
|
||||||
|
changes[i] = .init(diff_index);
|
||||||
|
try element_root.diffs.append(allocator, try tui_diff.elements.Diff(App, allocator, changes));
|
||||||
|
};
|
||||||
|
|
||||||
|
var root = try tui_diff.Container(App, allocator, &element_root, &element_tree);
|
||||||
defer root.deinit(); // also de-initializes the children
|
defer root.deinit(); // also de-initializes the children
|
||||||
|
element_root.container = &root;
|
||||||
|
|
||||||
try app.start();
|
try app.start();
|
||||||
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
||||||
// defer app.model.deinit();
|
|
||||||
|
|
||||||
// event loop
|
// event loop
|
||||||
loop: while (true) {
|
loop: while (true) {
|
||||||
|
|||||||
@@ -7,26 +7,77 @@
|
|||||||
// FIX known issues:
|
// FIX known issues:
|
||||||
|
|
||||||
content: []const u8,
|
content: []const u8,
|
||||||
changes: std.MultiArrayList(Change) = .empty,
|
changes: std.AutoArrayHashMapUnmanaged(Index, std.ArrayList(Index)) = .empty,
|
||||||
render_mode: enum {
|
render_mode: enum {
|
||||||
side_by_side,
|
side_by_side,
|
||||||
stacked,
|
stacked,
|
||||||
} = .stacked,
|
} = .stacked,
|
||||||
|
|
||||||
// TODO alloc-free parsing? (similar to how I've implemented the parser for `smd`?)
|
// TODO alloc-free parsing? (similar to how I've implemented the parser for `smd`?)
|
||||||
pub const init: Model = .{
|
// - parsing would tokenize and create a structure for the following patterns
|
||||||
.content = &.{},
|
// ```
|
||||||
};
|
// .content -> // initial content is ignored!
|
||||||
|
// (.file -> (.header -> .content)*)* // these are relevant
|
||||||
|
// -> .eof
|
||||||
|
// ```
|
||||||
|
// For each .file collect each pair of following .header and .content to a single hunk, if a new file follows then there there are no more changes for the previous file and the new .file changes will be collected till an .eof is found
|
||||||
|
pub fn init(gpa: Allocator, content: [:0]const u8) !Model {
|
||||||
|
if (content.len == 0) return error.EmptyDiff;
|
||||||
|
|
||||||
|
var this: Model = .{ .content = content };
|
||||||
|
var tokenizer = lexer.Tokenizer.init(content);
|
||||||
|
var token = tokenizer.next();
|
||||||
|
var file: Index = undefined;
|
||||||
|
var diff: Index = undefined;
|
||||||
|
var last_tag: lexer.Token.Tag = .invalid;
|
||||||
|
while (token.tag != .eof) : (token = tokenizer.next()) {
|
||||||
|
defer last_tag = token.tag;
|
||||||
|
switch (token.tag) {
|
||||||
|
.file => {
|
||||||
|
// create file
|
||||||
|
file = .{
|
||||||
|
.idx = token.loc.idx,
|
||||||
|
.len = token.loc.len,
|
||||||
|
};
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
.header => if (last_tag == .file or last_tag == .content) {
|
||||||
|
diff = .{
|
||||||
|
.idx = token.loc.idx,
|
||||||
|
.len = token.loc.len,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
.content => if (last_tag == .header) {
|
||||||
|
diff.len += token.loc.len;
|
||||||
|
const entry = try this.changes.getOrPut(gpa, file);
|
||||||
|
if (!entry.found_existing) entry.value_ptr.* = .empty;
|
||||||
|
try entry.value_ptr.append(gpa, diff);
|
||||||
|
},
|
||||||
|
else => unreachable,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.changes.entries.len == 0) return error.UnexpectedFormat;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(this: *Model, gpa: Allocator) void {
|
||||||
|
for (this.changes.values()) |*diff| diff.deinit(gpa);
|
||||||
|
this.changes.deinit(gpa);
|
||||||
|
}
|
||||||
|
|
||||||
pub const Index = struct {
|
pub const Index = struct {
|
||||||
idx: usize,
|
idx: usize,
|
||||||
len: usize,
|
len: usize,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Change = struct {
|
|
||||||
file: Index,
|
|
||||||
diff: Index,
|
|
||||||
};
|
|
||||||
|
|
||||||
const Model = @This();
|
const Model = @This();
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const lexer = @import("lexer.zig");
|
||||||
|
|
||||||
|
test {
|
||||||
|
std.testing.refAllDeclsRecursive(@This());
|
||||||
|
_ = @import("lexer.zig");
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,9 +4,11 @@
|
|||||||
|
|
||||||
// FIX known issues:
|
// FIX known issues:
|
||||||
|
|
||||||
pub const Event = union(enum) {};
|
pub const Event = union(enum) {
|
||||||
|
file: usize,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn Container(App: type, gpa: Allocator, element: *elements.Root(App), tree: *elements.Tree(App), diffs: *App.Scrollable) !App.Container {
|
pub fn Container(App: type, gpa: Allocator, element: *elements.Root(App), tree: *elements.Tree(App)) !App.Container {
|
||||||
var root: App.Container = try .init(gpa, .{
|
var root: App.Container = try .init(gpa, .{
|
||||||
.layout = .{
|
.layout = .{
|
||||||
.direction = .horizontal,
|
.direction = .horizontal,
|
||||||
@@ -16,7 +18,7 @@ pub fn Container(App: type, gpa: Allocator, element: *elements.Root(App), tree:
|
|||||||
},
|
},
|
||||||
}, element.element());
|
}, element.element());
|
||||||
try root.append(try .init(gpa, .{}, tree.element()));
|
try root.append(try .init(gpa, .{}, tree.element()));
|
||||||
try root.append(try .init(gpa, .{}, diffs.element()));
|
try root.append(try .init(gpa, .{}, .{})); // empty container holding the scrollable element for the diff of each file
|
||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user