Not a long time ago, I decided to learn zig and write my first text editor. However, I came across an issue where the UP
and DOWN
arrow keys don’t work as expected. I’ve tried many different variations of the implementation, but none of them worked as intended.
Here’s my text editor:
const std = @import("std");
const gap_buffer = @import("gap_buffer.zig");
const ncurses = @cImport({
@cInclude("ncurses.h");
});
pub const Editor = struct {
buf: gap_buffer.GapBuffer,
alloc: std.mem.Allocator,
render_buf: []u8,
pos_idx: usize = 0,
render_buf_dirty: bool = false,
pub fn init(allocator: std.mem.Allocator) std.mem.Allocator.Error!Editor {
return Editor{ .buf = try gap_buffer.GapBuffer.init(8, allocator), .alloc = allocator, .render_buf = try allocator.alloc(u8, 9) };
}
pub fn deinit(self: *Editor) void {
self.buf.deinit();
self.alloc.free(self.render_buf);
}
pub fn handle_key(self: *Editor, key: c_int) std.mem.Allocator.Error!void {
if (key == ncurses.KEY_LEFT) {
if (self.pos_idx > 0)
self.pos_idx -= 1;
} else if (key == ncurses.KEY_RIGHT) {
if (self.pos_idx < self.buf.buf.len - self.buf.gap_len)
self.pos_idx += 1;
} else if (key == ncurses.KEY_UP) {
try self.handle_up();
} else if (key == ncurses.KEY_DOWN) {
try self.handle_down();
} else if (key <= 255) {
self.render_buf_dirty = true;
try self.insert(@intCast(key));
}
}
pub fn render(self: *Editor) std.mem.Allocator.Error!void {
const t = self.buf;
if (t.gap_len == t.buf.len) return; // nothing to render
try self.refresh_render_buffer();
var y: u64 = 0;
var line_begin_idx: ?usize = null;
var line_len: usize = 0;
for (self.render_buf[0..self.pos_idx], 0..) |c, i| {
if (c == 'n') {
y += 1;
line_begin_idx = i;
} else {
line_len += 1;
}
}
var x: usize = undefined;
if (line_begin_idx == null) {
x = self.pos_idx;
} else if (self.pos_idx == line_begin_idx.?) {
x = line_len;
} else {
x = self.pos_idx - line_begin_idx.? - 1;
}
_ = ncurses.clear();
_ = ncurses.mvprintw(0, 0, @ptrCast(self.render_buf));
_ = ncurses.move(@intCast(y), @intCast(x));
_ = ncurses.refresh();
}
fn insert(self: *Editor, char: u8) std.mem.Allocator.Error!void {
self.buf.set_pos_idx(self.pos_idx);
try self.buf.insert(char);
self.pos_idx += 1;
}
fn handle_up(self: *Editor) std.mem.Allocator.Error!void {
try self.refresh_render_buffer(); // make sure the render buffer is updated
// the following code doesn't work:
// var curr_line_idx: usize = std.mem.lastIndexOf(u8, self.render_buf[0..self.pos_idx], "n") orelse return; // +
// var x: usize = undefined;
//
// if (self.render_buf[self.pos_idx] == 'n') {
// x = 0;
// curr_line_idx = self.pos_idx;
// } else {
// x = self.pos_idx - curr_line_idx - 1;
// }
//
// const prev_line_idx: ?usize = std.mem.lastIndexOf(u8, self.render_buf[0..curr_line_idx], "n"); // +
//
// if (prev_line_idx) |prev_idx| {
// self.pos_idx = prev_idx + @min(x, curr_line_idx - prev_idx - 1);
// return; // +
// }
//
// self.pos_idx = @min(x, curr_line_idx);
}
fn handle_down(self: *Editor) std.mem.Allocator.Error!void {
_ = self;
}
fn expand_render_buffer(self: *Editor) std.mem.Allocator.Error!void {
const t = self.buf;
const l = t.buf.len - t.gap_len;
if (self.render_buf.len >= l + 1)
return;
self.alloc.free(self.render_buf);
self.render_buf = try self.alloc.alloc(u8, l * 2 + 1);
}
fn refresh_render_buffer(self: *Editor) std.mem.Allocator.Error!void {
if (!self.render_buf_dirty)
return;
const t = self.buf;
try self.expand_render_buffer();
@memcpy(self.render_buf[0..t.gap_begin_idx], t.buf[0..t.gap_begin_idx]);
@memcpy(self.render_buf[t.gap_begin_idx .. t.buf.len - t.gap_len], t.buf[t.gap_begin_idx + t.gap_len ..]);
self.render_buf[t.buf.len - t.gap_len] = 0; // null terminator for c-printing
self.render_buf_dirty = false;
}
};
The functions where I need help are: handle_up
and handle_down
.
I’ve tried using a debugger such as lldb, but I couldn’t still figure out what was wrong, because every fix caused new problems.
Any hints on the implementation would be appreciated!