Widget is a handle type. Sort of a smart pointer. Comparing their addresses is brittle because it requires keeping Widget pointers stable. This is nonsense because Widget identity is actually determined by the actual widget object it points to. This big refactor elimits the requirement that Widget addresses remain stable to work properly with Widget.walk and Widget.get.
496 lines
18 KiB
Zig
496 lines
18 KiB
Zig
const std = @import("std");
|
|
const Allocator = std.mem.Allocator;
|
|
const tp = @import("thespian");
|
|
const tracy = @import("tracy");
|
|
const Diff = @import("diff").LineDiff;
|
|
const Kind = @import("diff").LineDiffKind;
|
|
const diffz = @import("diff").diffz;
|
|
const cbor = @import("cbor");
|
|
const root = @import("soft_root").root;
|
|
|
|
const Plane = @import("renderer").Plane;
|
|
const style = @import("renderer").style;
|
|
const styles = @import("renderer").styles;
|
|
const input = @import("input");
|
|
const command = @import("command");
|
|
const EventHandler = @import("EventHandler");
|
|
|
|
const Widget = @import("Widget.zig");
|
|
const MessageFilter = @import("MessageFilter.zig");
|
|
const tui = @import("tui.zig");
|
|
const ed = @import("editor.zig");
|
|
const DigitStyle = @import("config").DigitStyle;
|
|
const LineNumberMode = @import("config").LineNumberMode;
|
|
|
|
var width_global: usize = 0;
|
|
|
|
allocator: Allocator,
|
|
plane: Plane,
|
|
parent: Widget,
|
|
|
|
lines: u32 = 0,
|
|
view_rows: u32 = 1,
|
|
view_top: u32 = 1,
|
|
line: usize = 0,
|
|
line_number_mode: ?LineNumberMode = null,
|
|
line_number_style: DigitStyle,
|
|
highlight: bool,
|
|
symbols: bool,
|
|
width: usize,
|
|
editor: *ed.Editor,
|
|
editor_widget: ?Widget = null,
|
|
differ: diffz.AsyncDiffer,
|
|
|
|
const Self = @This();
|
|
|
|
pub fn create(allocator: Allocator, parent: Widget, event_source: Widget, editor: *ed.Editor) !Widget {
|
|
const self = try allocator.create(Self);
|
|
errdefer allocator.destroy(self);
|
|
self.* = .{
|
|
.allocator = allocator,
|
|
.plane = try Plane.init(&(Widget.Box{}).opts(@typeName(Self)), parent.plane.*),
|
|
.parent = parent,
|
|
.line_number_mode = tui.config().gutter_line_numbers_mode,
|
|
.line_number_style = tui.config().gutter_line_numbers_style,
|
|
.highlight = tui.config().highlight_current_line_gutter,
|
|
.symbols = tui.config().gutter_symbols,
|
|
.width = undefined,
|
|
.editor = editor,
|
|
.differ = try diffz.create(),
|
|
};
|
|
self.update_width();
|
|
try tui.message_filters().add(MessageFilter.bind(self, filter_receive));
|
|
try event_source.subscribe(EventHandler.bind(self, handle_event));
|
|
return self.widget();
|
|
}
|
|
|
|
pub fn widget(self: *Self) Widget {
|
|
return Widget.to(self);
|
|
}
|
|
|
|
pub fn deinit(self: *Self, allocator: Allocator) void {
|
|
self.diff_symbols_clear();
|
|
tui.message_filters().remove_ptr(self);
|
|
self.plane.deinit();
|
|
allocator.destroy(self);
|
|
}
|
|
|
|
fn diff_symbols_clear(self: *Self) void {
|
|
self.editor.changes.clearRetainingCapacity();
|
|
}
|
|
|
|
pub fn handle_event(self: *Self, _: tp.pid_ref, m: tp.message) tp.result {
|
|
if (try m.match(.{ "E", "open", tp.string, true, tp.more }))
|
|
return self.diff_update(false) catch |e| return tp.exit_error(e, @errorReturnTrace());
|
|
if (try m.match(.{ "E", "update" }))
|
|
return self.diff_update(self.lines == 0) catch |e| return tp.exit_error(e, @errorReturnTrace());
|
|
if (try m.match(.{ "E", "view", tp.extract(&self.lines), tp.extract(&self.view_rows), tp.extract(&self.view_top) }))
|
|
return self.update_width();
|
|
if (try m.match(.{ "E", "pos", tp.extract(&self.lines), tp.extract(&self.line), tp.more }))
|
|
return self.update_width();
|
|
if (try m.match(.{ "E", "close" })) {
|
|
self.lines = 0;
|
|
self.line = 0;
|
|
}
|
|
}
|
|
|
|
pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
|
|
var y: i32 = undefined;
|
|
var ypx: i32 = undefined;
|
|
var line_number_mode: ?LineNumberMode = null;
|
|
var line_number_style: DigitStyle = undefined;
|
|
|
|
if (try m.match(.{ "B", input.event.press, @intFromEnum(input.mouse.BUTTON1), tp.any, tp.any, tp.extract(&y), tp.any, tp.extract(&ypx) }))
|
|
return self.primary_click(y);
|
|
if (try m.match(.{ "B", input.event.press, @intFromEnum(input.mouse.BUTTON2), tp.any, tp.any, tp.extract(&y), tp.any, tp.extract(&ypx) }))
|
|
return self.middle_click();
|
|
if (try m.match(.{ "B", input.event.press, @intFromEnum(input.mouse.BUTTON3), tp.any, tp.any, tp.extract(&y), tp.any, tp.extract(&ypx) }))
|
|
return self.secondary_click();
|
|
if (try m.match(.{ "D", input.event.press, @intFromEnum(input.mouse.BUTTON1), tp.any, tp.any, tp.extract(&y), tp.any, tp.extract(&ypx) }))
|
|
return self.primary_drag(y);
|
|
if (try m.match(.{ "B", input.event.press, @intFromEnum(input.mouse.BUTTON4), tp.more }))
|
|
return self.mouse_click_button4();
|
|
if (try m.match(.{ "B", input.event.press, @intFromEnum(input.mouse.BUTTON5), tp.more }))
|
|
return self.mouse_click_button5();
|
|
if (try m.match(.{ "line_number_mode", tp.extract(&line_number_mode) })) {
|
|
self.line_number_mode = line_number_mode;
|
|
return false;
|
|
}
|
|
if (try m.match(.{ "line_number_style", tp.extract(&line_number_style) })) {
|
|
self.line_number_style = line_number_style;
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
fn update_width(self: *Self) void {
|
|
if (self.line_number_mode == .none) return;
|
|
const minimum = tui.config().gutter_width_minimum;
|
|
const maximum = tui.config().gutter_width_maximum;
|
|
const local = @max(int_width(self.lines), minimum);
|
|
const width =
|
|
switch (tui.config().gutter_width_mode) {
|
|
.local => local,
|
|
.global => blk: {
|
|
const width = @max(width_global, local);
|
|
width_global = @min(width, maximum);
|
|
break :blk width;
|
|
},
|
|
};
|
|
self.width = if (self.line_number_mode == .relative and width > minimum) minimum else @max(width, 2);
|
|
self.width += if (self.symbols) 3 else 1;
|
|
}
|
|
|
|
pub fn layout(self: *Self) Widget.Layout {
|
|
return .{ .static = self.get_width() };
|
|
}
|
|
|
|
inline fn get_width(self: *Self) usize {
|
|
return if (self.line_number_mode != .none) self.width else if (self.symbols) 3 else 1;
|
|
}
|
|
|
|
fn get_numbering_mode(self: *const Self) LineNumberMode {
|
|
return self.line_number_mode orelse switch (if (tui.input_mode()) |mode| mode.line_numbers else .absolute) {
|
|
.relative => .relative,
|
|
.inherit => if (tui.input_mode_outer()) |mode| from_mode_enum(mode.line_numbers) else .absolute,
|
|
.absolute => .absolute,
|
|
};
|
|
}
|
|
|
|
fn from_mode_enum(mode: anytype) LineNumberMode {
|
|
return switch (mode) {
|
|
.relative => .relative,
|
|
.inherit => .absolute,
|
|
.absolute => .absolute,
|
|
};
|
|
}
|
|
|
|
pub fn render(self: *Self, theme: *const Widget.Theme) bool {
|
|
const frame = tracy.initZone(@src(), .{ .name = "gutter render" });
|
|
defer frame.deinit();
|
|
self.plane.set_base_style(theme.editor);
|
|
self.plane.erase();
|
|
self.plane.home();
|
|
self.plane.set_style(theme.editor_gutter);
|
|
_ = self.plane.fill(" ");
|
|
switch (self.get_numbering_mode()) {
|
|
.none => self.render_none(theme),
|
|
.relative => self.render_relative(theme),
|
|
.absolute => self.render_linear(theme),
|
|
}
|
|
if (self.symbols)
|
|
self.render_diagnostics(theme);
|
|
return false;
|
|
}
|
|
|
|
pub fn render_none(self: *Self, theme: *const Widget.Theme) void {
|
|
var pos: usize = 0;
|
|
var linenum = self.view_top + 1;
|
|
var rows = self.view_rows;
|
|
var diff_symbols = self.editor.changes.items;
|
|
while (rows > 0) : (rows -= 1) {
|
|
if (linenum > self.lines) return;
|
|
if (self.highlight and linenum == self.line + 1)
|
|
self.render_line_highlight(pos, theme);
|
|
self.render_diff_symbols(&diff_symbols, pos, linenum, theme);
|
|
pos += 1;
|
|
linenum += 1;
|
|
}
|
|
}
|
|
|
|
pub fn render_linear(self: *Self, theme: *const Widget.Theme) void {
|
|
var pos: usize = 0;
|
|
var linenum = self.view_top + 1;
|
|
var rows = self.view_rows;
|
|
var diff_symbols = self.editor.changes.items;
|
|
while (rows > 0) : (rows -= 1) {
|
|
if (linenum > self.lines) return;
|
|
if (linenum == self.line + 1) {
|
|
self.plane.set_style(.{ .fg = theme.editor_gutter_active.fg });
|
|
self.plane.on_styles(styles.bold);
|
|
} else {
|
|
self.plane.set_style(.{ .fg = theme.editor_gutter.fg });
|
|
self.plane.off_styles(styles.bold);
|
|
}
|
|
self.plane.cursor_move_yx(@intCast(pos), 0);
|
|
try self.print_digits(linenum, self.line_number_style);
|
|
if (self.highlight and linenum == self.line + 1)
|
|
self.render_line_highlight(pos, theme);
|
|
self.render_diff_symbols(&diff_symbols, pos, linenum, theme);
|
|
pos += 1;
|
|
linenum += 1;
|
|
}
|
|
}
|
|
|
|
pub fn render_relative(self: *Self, theme: *const Widget.Theme) void {
|
|
const row: isize = @intCast(self.view_top + 1);
|
|
const line: isize = @intCast(self.line + 1);
|
|
var pos: usize = 0;
|
|
var linenum: isize = row - line;
|
|
var abs_linenum = self.view_top + 1;
|
|
var rows = self.view_rows;
|
|
var diff_symbols = self.editor.changes.items;
|
|
while (rows > 0) : (rows -= 1) {
|
|
if (self.lines > @as(u32, @intCast(row)) and pos > self.lines - @as(u32, @intCast(row))) return;
|
|
self.plane.set_style(if (linenum == 0) theme.editor_gutter_active else theme.editor_gutter);
|
|
const val = @abs(if (linenum == 0) line else linenum);
|
|
|
|
self.plane.cursor_move_yx(@intCast(pos), 0);
|
|
if (val > 999999)
|
|
_ = self.plane.print_aligned_right(@intCast(pos), "==> ", .{}) catch {}
|
|
else
|
|
self.print_digits(val, self.line_number_style) catch {};
|
|
|
|
if (self.highlight and linenum == 0)
|
|
self.render_line_highlight(pos, theme);
|
|
self.render_diff_symbols(&diff_symbols, pos, abs_linenum, theme);
|
|
pos += 1;
|
|
linenum += 1;
|
|
abs_linenum += 1;
|
|
}
|
|
}
|
|
|
|
inline fn render_line_highlight(self: *Self, pos: usize, theme: *const Widget.Theme) void {
|
|
for (0..self.get_width()) |i| {
|
|
self.plane.cursor_move_yx(@intCast(pos), @intCast(i));
|
|
var cell = self.plane.cell_init();
|
|
_ = self.plane.at_cursor_cell(&cell) catch return;
|
|
cell.set_style_bg(theme.editor_line_highlight);
|
|
_ = self.plane.putc(&cell) catch {};
|
|
}
|
|
}
|
|
|
|
inline fn render_diff_symbols(self: *Self, diff_symbols: *[]Diff, pos: usize, linenum_: usize, theme: *const Widget.Theme) void {
|
|
const linenum = linenum_ - 1;
|
|
if (diff_symbols.len == 0) return;
|
|
while ((diff_symbols.*)[0].line < linenum) {
|
|
diff_symbols.* = (diff_symbols.*)[1..];
|
|
if (diff_symbols.len == 0) return;
|
|
}
|
|
|
|
if ((diff_symbols.*)[0].line > linenum) return;
|
|
|
|
while ((diff_symbols.*)[0].line == linenum) {
|
|
self.render_diff((diff_symbols.*)[0], pos, theme);
|
|
diff_symbols.* = (diff_symbols.*)[1..];
|
|
if (diff_symbols.len == 0) return;
|
|
}
|
|
}
|
|
|
|
inline fn render_diff(self: *Self, sym: Diff, pos: usize, theme: *const Widget.Theme) void {
|
|
const char = switch (sym.kind) {
|
|
.insert => "┃",
|
|
.modify => "┃",
|
|
.delete => "▔",
|
|
};
|
|
|
|
var lines = if (sym.kind == .delete) 1 else sym.lines;
|
|
while (lines > 0) : (lines -= 1) {
|
|
self.plane.cursor_move_yx(@intCast(pos + lines - 1), @intCast(self.get_width() - 1));
|
|
var cell = self.plane.cell_init();
|
|
_ = self.plane.at_cursor_cell(&cell) catch return;
|
|
cell.set_style_fg(switch (sym.kind) {
|
|
.insert => theme.editor_gutter_added,
|
|
.modify => theme.editor_gutter_modified,
|
|
.delete => theme.editor_gutter_deleted,
|
|
});
|
|
_ = self.plane.cell_load(&cell, char) catch {};
|
|
_ = self.plane.putc(&cell) catch {};
|
|
}
|
|
}
|
|
|
|
fn render_diagnostics(self: *Self, theme: *const Widget.Theme) void {
|
|
if (tui.config().inline_diagnostics)
|
|
for (self.editor.diagnostics.items) |*diag| self.render_diagnostic(diag, theme);
|
|
}
|
|
|
|
fn render_diagnostic(self: *Self, diag: *const ed.Diagnostic, theme: *const Widget.Theme) void {
|
|
const row = diag.sel.begin.row;
|
|
if (!(self.view_top < row and row < self.view_top + self.view_rows)) return;
|
|
const style_ = switch (diag.get_severity()) {
|
|
.Error => theme.editor_error,
|
|
.Warning => theme.editor_warning,
|
|
.Information => theme.editor_information,
|
|
.Hint => theme.editor_hint,
|
|
};
|
|
const icon = switch (diag.get_severity()) {
|
|
.Error => "",
|
|
.Warning => "",
|
|
.Information => "",
|
|
.Hint => "",
|
|
};
|
|
const y = row - self.view_top;
|
|
self.plane.cursor_move_yx(@intCast(y), 0);
|
|
var cell = self.plane.cell_init();
|
|
_ = self.plane.at_cursor_cell(&cell) catch return;
|
|
cell.set_style_fg(style_);
|
|
_ = self.plane.cell_load(&cell, icon) catch {};
|
|
_ = self.plane.putc(&cell) catch {};
|
|
}
|
|
|
|
fn focus_editor(self: *Self) void {
|
|
const editor_widget = self.editor_widget orelse blk: {
|
|
const editor_widget = self.parent.get("editor") orelse return;
|
|
self.editor_widget = editor_widget;
|
|
break :blk editor_widget;
|
|
};
|
|
_ = tui.set_focus_by_widget(editor_widget);
|
|
}
|
|
|
|
fn primary_click(self: *Self, y_: i32) error{Exit}!bool {
|
|
self.focus_editor();
|
|
const y = self.editor.plane.abs_y_to_rel(y_);
|
|
var line = self.view_top + 1;
|
|
line += @intCast(y);
|
|
if (line > self.lines) line = self.lines;
|
|
try command.executeName("goto_line", command.fmt(.{line}));
|
|
try command.executeName("goto_column", command.fmt(.{1}));
|
|
try command.executeName("select_end", .{});
|
|
try command.executeName("select_right", .{});
|
|
return true;
|
|
}
|
|
|
|
fn primary_drag(self: *Self, y_: i32) error{Exit}!bool {
|
|
self.focus_editor();
|
|
const y = self.editor.plane.abs_y_to_rel(y_);
|
|
try command.executeName("drag_to", command.fmt(.{ y + 1, 0 }));
|
|
return true;
|
|
}
|
|
|
|
fn secondary_click(_: *Self) error{Exit}!bool {
|
|
try command.executeName("gutter_mode_next", .{});
|
|
return true;
|
|
}
|
|
|
|
fn middle_click(_: *Self) error{Exit}!bool {
|
|
try command.executeName("gutter_style_next", .{});
|
|
return true;
|
|
}
|
|
|
|
fn mouse_click_button4(self: *Self) error{Exit}!bool {
|
|
self.editor.mouse_scroll_up();
|
|
return true;
|
|
}
|
|
|
|
fn mouse_click_button5(self: *Self) error{Exit}!bool {
|
|
self.editor.mouse_scroll_down();
|
|
return true;
|
|
}
|
|
|
|
fn diff_update(self: *Self, clear: bool) !void {
|
|
if (clear or self.lines > root.max_diff_lines) {
|
|
self.diff_symbols_clear();
|
|
return;
|
|
}
|
|
const ctx = try self.allocator.create(DiffContext);
|
|
ctx.* = .{
|
|
.allocator = self.allocator,
|
|
.file_path = try self.allocator.dupe(u8, self.editor.file_path orelse ""),
|
|
};
|
|
return self.differ.diff_buffer(diff_result, @intFromPtr(ctx), self.editor.buffer orelse return);
|
|
}
|
|
|
|
const DiffContext = struct {
|
|
allocator: std.mem.Allocator,
|
|
file_path: []u8,
|
|
|
|
fn deinit(self: *@This()) void {
|
|
self.allocator.free(self.file_path);
|
|
self.allocator.destroy(self);
|
|
}
|
|
};
|
|
|
|
fn diff_result(from: tp.pid_ref, data: usize, edits: []Diff) void {
|
|
const ctx: *DiffContext = @ptrFromInt(data);
|
|
defer ctx.deinit();
|
|
diff_result_send(from, ctx, edits) catch |e| @import("log").err(@typeName(Self), "diff", e);
|
|
}
|
|
|
|
fn diff_result_send(from: tp.pid_ref, ctx: *DiffContext, edits: []Diff) !void {
|
|
var buf: std.Io.Writer.Allocating = .init(std.heap.c_allocator);
|
|
defer buf.deinit();
|
|
try cbor.writeArrayHeader(&buf.writer, 3);
|
|
try cbor.writeValue(&buf.writer, "DIFF");
|
|
try cbor.writeValue(&buf.writer, ctx.file_path);
|
|
try cbor.writeArrayHeader(&buf.writer, edits.len);
|
|
for (edits) |edit| {
|
|
try cbor.writeArrayHeader(&buf.writer, 3);
|
|
try cbor.writeValue(&buf.writer, switch (edit.kind) {
|
|
.insert => "I",
|
|
.modify => "M",
|
|
.delete => "D",
|
|
});
|
|
try cbor.writeValue(&buf.writer, edit.line);
|
|
try cbor.writeValue(&buf.writer, edit.lines);
|
|
}
|
|
from.send_raw(tp.message{ .buf = buf.written() }) catch return;
|
|
}
|
|
|
|
pub fn process_diff(self: *Self, cb: []const u8) MessageFilter.Error!void {
|
|
var iter = cb;
|
|
self.diff_symbols_clear();
|
|
var count = try cbor.decodeArrayHeader(&iter);
|
|
while (count > 0) : (count -= 1) {
|
|
var line: usize = undefined;
|
|
var lines: usize = undefined;
|
|
if (try cbor.matchValue(&iter, .{ "I", cbor.extract(&line), cbor.extract(&lines) })) {
|
|
try self.process_edit(.insert, line, lines);
|
|
continue;
|
|
}
|
|
if (try cbor.matchValue(&iter, .{ "M", cbor.extract(&line), cbor.extract(&lines) })) {
|
|
try self.process_edit(.modify, line, lines);
|
|
continue;
|
|
}
|
|
if (try cbor.matchValue(&iter, .{ "D", cbor.extract(&line), cbor.extract(&lines) })) {
|
|
try self.process_edit(.delete, line, lines);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn process_edit(self: *Self, kind: Kind, line: usize, lines: usize) !void {
|
|
(try self.editor.changes.addOne(self.allocator)).* = .{ .kind = kind, .line = line, .lines = lines };
|
|
}
|
|
|
|
pub fn filter_receive(self: *Self, _: tp.pid_ref, m: tp.message) MessageFilter.Error!bool {
|
|
var file_path: []const u8 = undefined;
|
|
var cb: []const u8 = undefined;
|
|
if (cbor.match(m.buf, .{ "DIFF", tp.extract(&file_path), tp.extract_cbor(&cb) }) catch false) {
|
|
if (std.mem.eql(u8, file_path, self.editor.file_path orelse return false)) {
|
|
try self.process_diff(cb);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
fn int_width(n_: usize) usize {
|
|
var n = n_;
|
|
var size: usize = 1;
|
|
while (true) {
|
|
n /= 10;
|
|
if (n == 0) return size;
|
|
size += 1;
|
|
}
|
|
}
|
|
|
|
fn print_digits(self: *Self, n_: anytype, style_: DigitStyle) !void {
|
|
var n = n_;
|
|
var buf: [12][]const u8 = undefined;
|
|
var digits: std.ArrayListUnmanaged([]const u8) = .initBuffer(&buf);
|
|
while (true) {
|
|
digits.addOneAssumeCapacity().* = get_digit(n % 10, style_);
|
|
n /= 10;
|
|
if (n == 0) break;
|
|
}
|
|
std.mem.reverse([]const u8, digits.items);
|
|
self.plane.cursor_move_yx(@intCast(self.plane.cursor_y()), @intCast(self.width -| digits.items.len -| 1));
|
|
for (digits.items) |digit| _ = try self.plane.putstr(digit);
|
|
}
|
|
|
|
pub fn print_digit(plane: *Plane, n: anytype, style_: DigitStyle) !void {
|
|
_ = try plane.putstr(get_digit(n, style_));
|
|
}
|
|
|
|
const get_digit = @import("fonts.zig").get_digit;
|